Default to noop tokenManager and no backend secret

Signed-off-by: Nataliya Issayeva <nissayeva@users.noreply.github.com>
This commit is contained in:
Nataliya Issayeva
2021-11-12 15:41:21 -05:00
parent c35abaadee
commit 83aa387b62
9 changed files with 103 additions and 34 deletions
+3 -2
View File
@@ -24,8 +24,9 @@ app:
backend:
# Used for enabling authorization, secret is shared by all backend plugins
authorization:
secret: ${BACKEND_SECRET}
# See authenticate-api-requests.md in the contrib docs for information on the format
# authorization:
# secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7000
listen:
port: 7000
@@ -69,8 +69,10 @@ async function main() {
req.cookies['token'];
// Authenticate all requests originating from backends by default
const isServerToken = await authEnv.tokenManager.isServerToken(token);
if (!isServerToken) {
const isValidServerToken = await authEnv.tokenManager.validateServerToken(
token,
);
if (!isValidServerToken) {
req.user = await identity.authenticate(token);
}
@@ -275,7 +277,25 @@ export const plugin = createPlugin({
});
```
In the (probably unlikely) case that you need to authenticate from a backend plugin, the plugin environment contains a `tokenManager` that will provide a server token to use in the request:
In the (probably unlikely) case that you need to authenticate from a backend plugin, the plugin environment contains a `tokenManager` that will provide a server token to use in the request. It is a noop `tokenManager` by default -- you'll need to instantiate a new one using the secret from your config instead:
```diff
// packages/backend/src/index.ts
function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = SingleHostDiscovery.fromConfig(config);
root.info(`Created UrlReader ${reader}`);
const cacheManager = CacheManager.fromConfig(config);
const databaseManager = DatabaseManager.fromConfig(config);
- const tokenManager = ServerTokenManager.noop();
+ const tokenManager = ServerTokenManager.fromConfig(config);
```
With this `tokenManager`, you can then generate a server token for requests:
```
const { token } = await this.tokenManager.getServerToken();
+9
View File
@@ -20,6 +20,15 @@ export interface Config {
};
backend: {
/** Backend configuration for when request authentication is enabled */
authorization?: {
/**
* Secret shared by all backends for generating tokens
* Format is base64 24-bit key
*/
secret?: string;
};
baseUrl: string; // defined in core, but repeated here without doc
/** Address that the backend should listen to. */
@@ -22,32 +22,49 @@ const configWithSecret = new ConfigReader({
});
describe('ServerTokenManager', () => {
it('should throw if secret in config does not exist', () => {
expect(() => ServerTokenManager.fromConfig(emptyConfig)).toThrowError();
});
describe('getServerToken', () => {
it('should return a token if secret in config exists', async () => {
const tokenManager = new ServerTokenManager(configWithSecret);
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
expect((await tokenManager.getServerToken()).token).toBeDefined();
});
it('should return a token if secret in config does not exist', async () => {
const tokenManagerWithEmptyConfig = new ServerTokenManager(emptyConfig);
expect(
(await tokenManagerWithEmptyConfig.getServerToken()).token,
).toBeDefined();
it('should return an empty string if using a noop TokenManager', async () => {
const tokenManager = ServerTokenManager.noop();
expect((await tokenManager.getServerToken()).token).toBe('');
});
});
describe('isServerToken', () => {
describe('validateServerToken', () => {
it('should return true if token is valid', async () => {
const tokenManager = new ServerTokenManager(configWithSecret);
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
const { token } = await tokenManager.getServerToken();
const isServerToken = await tokenManager.isServerToken(token);
expect(isServerToken).toBe(true);
const isValidServerToken = await tokenManager.validateServerToken(token);
expect(isValidServerToken).toBe(true);
});
it('should return false if token is invalid', async () => {
const tokenManager = new ServerTokenManager(configWithSecret);
const isServerToken = await tokenManager.isServerToken('random-string');
expect(isServerToken).toBe(false);
const tokenManager = ServerTokenManager.fromConfig(configWithSecret);
const isValidServerToken = await tokenManager.validateServerToken(
'random-string',
);
expect(isValidServerToken).toBe(false);
});
it('should always return true if using noop TokenManager', async () => {
const tokenManager = ServerTokenManager.noop();
const { token } = await tokenManager.getServerToken();
const isValidServerToken0 = await tokenManager.validateServerToken(token);
const isValidServerToken1 = await tokenManager.validateServerToken(
'random-string',
);
const isValidServerToken2 = await tokenManager.validateServerToken('');
expect(isValidServerToken0).toBe(true);
expect(isValidServerToken1).toBe(true);
expect(isValidServerToken2).toBe(true);
});
});
});
@@ -19,15 +19,40 @@ import { Config } from '@backstage/config';
import { TokenManager } from './types';
export class ServerTokenManager implements TokenManager {
private key: JWK.OctKey;
private key: JWK.OctKey | JWK.NoneKey;
constructor(config: Config) {
const secret =
config.getOptionalString('backend.authorization.secret') ?? 'no-secret';
this.key = JWK.asKey({ kty: 'oct', k: secret });
static noop() {
return new ServerTokenManager();
}
async isServerToken(token: string): Promise<boolean> {
static fromConfig(config: Config) {
const secret = config.getOptionalString('backend.authorization.secret');
if (!secret) {
throw new Error('No backend auth secret set in app-config');
}
return new ServerTokenManager(secret);
}
private constructor(secret: string = '') {
this.key = secret ? JWK.asKey({ kty: 'oct', k: secret }) : JWK.None;
}
async getServerToken(): Promise<{ token: string }> {
if (this.key === JWK.None) {
return { token: '' };
}
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
algorithm: 'HS256',
});
return { token: jwt };
}
async validateServerToken(token: string): Promise<boolean> {
if (this.key === JWK.None) {
return true;
}
try {
JWT.verify(token, this.key);
return true;
@@ -35,11 +60,4 @@ export class ServerTokenManager implements TokenManager {
return false;
}
}
async getServerToken(): Promise<{ token: string }> {
const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, {
algorithm: 'HS256',
});
return { token: jwt };
}
}
+1 -1
View File
@@ -15,6 +15,6 @@
*/
export interface TokenManager {
isServerToken: (token: string) => Promise<boolean>;
getServerToken: () => Promise<{ token: string }>;
validateServerToken: (token: string) => Promise<boolean>;
}
+1 -1
View File
@@ -61,7 +61,7 @@ function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = SingleHostDiscovery.fromConfig(config);
const tokenManager = new ServerTokenManager(config);
const tokenManager = ServerTokenManager.noop();
root.info(`Created UrlReader ${reader}`);
@@ -6,6 +6,10 @@ organization:
name: My Company
backend:
# Used for enabling authorization, secret is shared by all backend plugins
# See authenticate-api-requests.md in the contrib docs for information on the format
# authorization:
# secret: ${BACKEND_SECRET}
baseUrl: http://localhost:7000
listen:
port: 7000
@@ -38,7 +38,7 @@ function makeCreateEnv(config: Config) {
const cacheManager = CacheManager.fromConfig(config);
const databaseManager = DatabaseManager.fromConfig(config);
const tokenManager = new ServerTokenManager(config);
const tokenManager = ServerTokenManager.noop();
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });