Mandate auth token to connect over TCP
Summary: Until now, launching flipper-server with TCP would accept any incoming connection as long as it comes from the same origin (localhost) using web socket host origin verification. This is not entirely secure as origin can be spoofed with tools like curl. Our team created a security review and a proposal was written: https://docs.google.com/document/d/16iXypCQibPiner061SoaQUFUY9tLVAEpkKfV_hUXI7c/ Effectively, Flipper can generate a token which is then used by the client to authenticate. This diff contains the changes required to generate, obtain, and validate authentication tokens from clients connecting to flipper over TCP connections. The token itself is a JWT token. JWT was chosen because it is a simple industry standard which offers three features which can immediately benefit us: - Expiration handling. No need for Flipper to store this information anywhere. - Payload. Payload can be used to push any data we deem relevant i.e. unix username. - Signing. Signed and verified using the same server key pair which is already in place for certificate exchange. Additionally, the token is stored in the Flipper static folder. This ensures that the browser and PWA clients have access to it. Reviewed By: mweststrate Differential Revision: D45179654 fbshipit-source-id: 6761bcb24f4ba30b67d1511cde8fe875158d78af
This commit is contained in:
committed by
Facebook GitHub Bot
parent
70cdc9bedc
commit
238f40f55d
@@ -9,6 +9,7 @@
|
||||
|
||||
import {promisify} from 'util';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import {
|
||||
openssl,
|
||||
isInstalled as opensslInstalled,
|
||||
@@ -18,13 +19,15 @@ import tmp, {FileOptions} from 'tmp';
|
||||
import {reportPlatformFailures} from 'flipper-common';
|
||||
import {isTest} from 'flipper-common';
|
||||
import {flipperDataFolder} from './paths';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {getFlipperServerConfig} from '../FlipperServerConfig';
|
||||
|
||||
const tmpFile = promisify(tmp.file) as (
|
||||
options?: FileOptions,
|
||||
) => Promise<string>;
|
||||
|
||||
const getFilePath = (fileName: string): string => {
|
||||
return path.resolve(flipperDataFolder, 'certs', fileName);
|
||||
const getFilePath = (filename: string): string => {
|
||||
return path.resolve(flipperDataFolder, 'certs', filename);
|
||||
};
|
||||
|
||||
// Desktop file paths
|
||||
@@ -69,16 +72,23 @@ export const ensureOpenSSLIsAvailable = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
let serverConfig: SecureServerConfig | undefined;
|
||||
export const loadSecureServerConfig = async (): Promise<SecureServerConfig> => {
|
||||
if (serverConfig) {
|
||||
return serverConfig;
|
||||
}
|
||||
|
||||
await ensureOpenSSLIsAvailable();
|
||||
await certificateSetup();
|
||||
return {
|
||||
await generateAuthToken();
|
||||
serverConfig = {
|
||||
key: await fs.readFile(serverKey),
|
||||
cert: await fs.readFile(serverCert),
|
||||
ca: await fs.readFile(caCert),
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true, // can be false if necessary as we don't strictly need to verify the client
|
||||
};
|
||||
return serverConfig;
|
||||
};
|
||||
|
||||
export const extractAppNameFromCSR = async (csr: string): Promise<string> => {
|
||||
@@ -254,3 +264,44 @@ const writeToTempFile = async (content: string): Promise<string> => {
|
||||
await fs.writeFile(path, content);
|
||||
return path;
|
||||
};
|
||||
|
||||
const getStaticFilePath = (filename: string): string => {
|
||||
return path.resolve(getFlipperServerConfig().paths.staticPath, filename);
|
||||
};
|
||||
|
||||
const tokenFilename = 'auth.token';
|
||||
const getTokenPath = (): string => {
|
||||
const path = getStaticFilePath(tokenFilename);
|
||||
return path;
|
||||
};
|
||||
|
||||
export const generateAuthToken = async () => {
|
||||
const privateKey = await fs.readFile(serverKey);
|
||||
const token = jwt.sign({unixname: os.userInfo().username}, privateKey, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '21 days',
|
||||
});
|
||||
|
||||
await fs.writeFile(getTokenPath(), token);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export const getAuthToken = async () => {
|
||||
if (!(await fs.pathExists(getTokenPath()))) {
|
||||
return generateAuthToken();
|
||||
}
|
||||
|
||||
const token = await fs.readFile(getTokenPath());
|
||||
return token.toString();
|
||||
};
|
||||
|
||||
export const validateAuthToken = (token: string) => {
|
||||
if (!serverConfig) {
|
||||
throw new Error(
|
||||
'Unable to validate auth token as no server configuration is available',
|
||||
);
|
||||
}
|
||||
|
||||
jwt.verify(token, serverConfig.cert);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user