diff --git a/app-config.yaml b/app-config.yaml index d18b326c2c..d89961c9f9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -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 diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index d25e4a832a..b58c48f988 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -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(); diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 3690069431..865fbb86db 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -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. */ diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts index 246a3199f5..ddbb62bd42 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.test.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -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); }); }); }); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.ts b/packages/backend-common/src/tokens/ServerTokenManager.ts index 6737dd4d8b..8c757f5ff6 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.ts @@ -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 { + 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 { + 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 }; - } } diff --git a/packages/backend-common/src/tokens/types.ts b/packages/backend-common/src/tokens/types.ts index 278695a448..506e1191ec 100644 --- a/packages/backend-common/src/tokens/types.ts +++ b/packages/backend-common/src/tokens/types.ts @@ -15,6 +15,6 @@ */ export interface TokenManager { - isServerToken: (token: string) => Promise; getServerToken: () => Promise<{ token: string }>; + validateServerToken: (token: string) => Promise; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 63285c83fa..4426ed7767 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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}`); diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 9c35c58c33..726d31ea65 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -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 diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 87dc390291..3f12122a3f 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -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 });