diff --git a/app-config.yaml b/app-config.yaml index d3dad59c2f..913a72dbab 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -26,7 +26,8 @@ backend: # Used for enabling authentication, secret is shared by all backend plugins # See backend-to-backend-auth.md in the docs for information on the format # auth: - # secret: ${BACKEND_SECRET} + # keys: + # - secret: ${BACKEND_SECRET} baseUrl: http://localhost:7007 listen: port: 7007 diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 3ba3d83ebc..bdc7740c13 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -22,13 +22,16 @@ export interface Config { backend: { /** Backend configuration for when request authentication is enabled */ auth?: { - /** - * Secret shared by all backends for generating tokens. Should be - * a base64 string, recommended length is 24 bytes. - * - * @visibility secret - */ - secret: string; + /** Keys shared by all backends for signing and validating backend tokens. */ + keys: { + /** + * Secret for generating tokens. Should be a base64 string, recommended + * length is 24 bytes. + * + * @visibility secret + */ + secret: string; + }[]; }; baseUrl: string; // defined in core, but repeated here without doc diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts index 20e18e9080..c00f708179 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.test.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -18,7 +18,7 @@ import { ServerTokenManager } from './ServerTokenManager'; const emptyConfig = new ConfigReader({}); const configWithSecret = new ConfigReader({ - backend: { auth: { secret: 'a-secret-key' } }, + backend: { auth: { keys: [{ secret: 'a-secret-key' }] } }, }); describe('ServerTokenManager', () => { @@ -61,12 +61,42 @@ describe('ServerTokenManager', () => { expect(() => tokenManager2.validateToken(token)).not.toThrow(); }); - it('should throw for server tokens created using a different secret', async () => { + it('should validate server tokens created using any of the secrets', async () => { const tokenManager1 = ServerTokenManager.fromConfig( - new ConfigReader({ backend: { auth: { secret: 'a1b2c3' } } }), + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a1b2c3' }] } }, + }), ); const tokenManager2 = ServerTokenManager.fromConfig( - new ConfigReader({ backend: { auth: { secret: 'd4e5f6' } } }), + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'd4e5f6' }] } }, + }), + ); + const tokenManager3 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { + auth: { keys: [{ secret: 'a1b2c3' }, { secret: 'd4e5f6' }] }, + }, + }), + ); + + const { token: token1 } = await tokenManager1.getToken(); + expect(() => tokenManager3.validateToken(token1)).not.toThrow(); + + const { token: token2 } = await tokenManager2.getToken(); + expect(() => tokenManager3.validateToken(token2)).not.toThrow(); + }); + + it('should throw for server tokens created using a different secret', async () => { + const tokenManager1 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a1b2c3' }] } }, + }), + ); + const tokenManager2 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'd4e5f6' }] } }, + }), ); const { token } = await tokenManager1.getToken(); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.ts b/packages/backend-common/src/tokens/ServerTokenManager.ts index e4afc2997e..ea79497b42 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JWK, JWT } from 'jose'; +import { JWKS, JWK, JWT } from 'jose'; import { Config } from '@backstage/config'; import { AuthenticationError } from '@backstage/errors'; import { TokenManager } from './types'; @@ -26,40 +26,47 @@ import { TokenManager } from './types'; * @public */ export class ServerTokenManager implements TokenManager { - private key: JWK.OctKey | JWK.NoneKey; + private readonly keyStore: JWKS.KeyStore; static noop() { return new ServerTokenManager(); } static fromConfig(config: Config) { - const secret = config.getString('backend.auth.secret'); - - return new ServerTokenManager(secret); + return new ServerTokenManager( + config + .getConfigArray('backend.auth.keys') + .map(key => key.getString('secret')), + ); } - private constructor(secret: string = '') { - this.key = secret ? JWK.asKey({ kty: 'oct', k: secret }) : JWK.None; + private constructor(secrets?: string[]) { + this.keyStore = new JWKS.KeyStore( + secrets?.length + ? secrets.map(secret => JWK.asKey({ kty: 'oct', k: secret })) + : [], + ); } async getToken(): Promise<{ token: string }> { - if (this.key === JWK.None) { + if (this.keyStore.size === 0) { return { token: '' }; } - const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, { + const jwt = JWT.sign({ sub: 'backstage-server' }, this.keyStore.all()[0], { algorithm: 'HS256', }); + return { token: jwt }; } validateToken(token: string): void { - if (this.key === JWK.None) { + if (this.keyStore.size === 0) { return; } try { - JWT.verify(token, this.key); + JWT.verify(token, this.keyStore); return; } catch (e) { throw new AuthenticationError('Invalid server token');