From b38d77f4f0f9ebc2cda7a43bc1031549d7458cd3 Mon Sep 17 00:00:00 2001 From: Nataliya Issayeva Date: Tue, 9 Nov 2021 17:57:03 -0500 Subject: [PATCH] Change AuthIdentityTokenManager to ServerTokenManager Co-authored-by: Joe Porpeglia Co-authored-by: Tim Hansen Signed-off-by: Nataliya Issayeva --- app-config.yaml | 2 +- .../src/identity/IdentityClient.ts | 3 - .../tokens/AuthIdentityTokenManager.test.ts | 70 ----------------- .../src/tokens/AuthIdentityTokenManager.ts | 76 ------------------- .../src/tokens/ServerTokenManager.test.ts | 49 ++++++++++++ .../src/tokens/ServerTokenManager.ts | 45 +++++++++++ packages/backend-common/src/tokens/index.ts | 2 +- packages/backend-common/src/tokens/types.ts | 2 +- packages/backend/src/index.ts | 9 ++- packages/backend/src/types.ts | 4 +- .../default-app/packages/backend/src/index.ts | 4 +- .../default-app/packages/backend/src/types.ts | 4 +- 12 files changed, 109 insertions(+), 161 deletions(-) delete mode 100644 packages/backend-common/src/tokens/AuthIdentityTokenManager.test.ts delete mode 100644 packages/backend-common/src/tokens/AuthIdentityTokenManager.ts create mode 100644 packages/backend-common/src/tokens/ServerTokenManager.test.ts create mode 100644 packages/backend-common/src/tokens/ServerTokenManager.ts diff --git a/app-config.yaml b/app-config.yaml index fdd97cbb8c..88cd2c4648 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -23,7 +23,7 @@ app: title: '#backstage' backend: - auth: + authorization: secret: ${BACKEND_SECRET} baseUrl: http://localhost:7000 listen: diff --git a/packages/backend-common/src/identity/IdentityClient.ts b/packages/backend-common/src/identity/IdentityClient.ts index c2aa913df3..2921b5705c 100644 --- a/packages/backend-common/src/identity/IdentityClient.ts +++ b/packages/backend-common/src/identity/IdentityClient.ts @@ -28,8 +28,6 @@ const CLOCK_MARGIN_S = 10; * @experimental This is not a stable API yet */ -// TODO: (b2b-auth) move IdentityClient into tokens -// perhaps also create an interface? export class IdentityClient { private readonly discovery: PluginEndpointDiscovery; private readonly issuer: string; @@ -93,7 +91,6 @@ export class IdentityClient { * Returns the public signing key matching the given jwt token, * or null if no matching key was found */ - // TODO (b2b-auth): switch on type to identify server tokens? private async getKey(rawJwtToken: string): Promise { const { header, payload } = JWT.decode(rawJwtToken, { complete: true, diff --git a/packages/backend-common/src/tokens/AuthIdentityTokenManager.test.ts b/packages/backend-common/src/tokens/AuthIdentityTokenManager.test.ts deleted file mode 100644 index c0e952bae5..0000000000 --- a/packages/backend-common/src/tokens/AuthIdentityTokenManager.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ConfigReader } from '@backstage/core-app-api'; -import { PluginEndpointDiscovery } from '..'; -import { IdentityClient } from '../identity'; -import { AuthIdentityTokenManager } from './AuthIdentityTokenManager'; - -const discovery: PluginEndpointDiscovery = { - async getBaseUrl() { - return 'url'; - }, - async getExternalBaseUrl() { - return 'url'; - }, -}; -const config = new ConfigReader({ - backend: { auth: { secret: 'a-secret-key' } }, -}); - -beforeAll(() => { - jest - .spyOn(IdentityClient.prototype, 'authenticate') - .mockImplementation(async (_token?: string) => { - throw new Error('No'); - }); -}); - -describe('AuthIdentityTokenManager', () => { - it('should throw in getServerToken if there is no secret in the config', async () => { - const emptyConfig = new ConfigReader({}); - const tokenManager = new AuthIdentityTokenManager(discovery, emptyConfig); - await expect(tokenManager.getServerToken()).rejects.toThrow( - 'No server token defined in config', - ); - }); - - it('should validate a valid server token', async () => { - const tokenManager = new AuthIdentityTokenManager(discovery, config); - const { token } = await tokenManager.getServerToken(); - await expect(tokenManager.validateToken(token)).resolves.toBeUndefined(); - }); - - it('should reject an invalid server token', async () => { - const differentConfig = new ConfigReader({ - backend: { auth: { secret: 'a-different-key' } }, - }); - const tokenManager = new AuthIdentityTokenManager(discovery, config); - const differentTokenManager = new AuthIdentityTokenManager( - discovery, - differentConfig, - ); - const { token } = await tokenManager.getServerToken(); - await expect(differentTokenManager.validateToken(token)).rejects.toThrow( - 'Invalid token', - ); - }); -}); diff --git a/packages/backend-common/src/tokens/AuthIdentityTokenManager.ts b/packages/backend-common/src/tokens/AuthIdentityTokenManager.ts deleted file mode 100644 index f8c3fe061d..0000000000 --- a/packages/backend-common/src/tokens/AuthIdentityTokenManager.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { JWK, JWT } from 'jose'; -import { Config } from '@backstage/config'; -import { TokenManager } from './types'; -import { IdentityClient } from '../identity'; -import { PluginEndpointDiscovery } from '../discovery'; - -// TODO: (b2b-auth) rename this class -export class AuthIdentityTokenManager implements TokenManager { - private identityClient: IdentityClient; - private key?: JWK.OctKey; - - constructor(discovery: PluginEndpointDiscovery, config: Config) { - this.identityClient = new IdentityClient({ - discovery: discovery, - issuer: 'auth-identity-token-manager', - }); - - const secret = config.getOptionalString('backend.auth.secret'); - if (secret) { - this.key = JWK.asKey({ kty: 'oct', k: secret }); - } - } - - async getServerToken(): Promise<{ token: string }> { - if (!this.key) { - throw new Error('No server token defined in config'); - } - - const jwt = JWT.sign({ sub: 'backstage-server' }, this.key, { - algorithm: 'HS256', - }); - return { token: jwt }; - } - - // TODO: (b2b-auth) authenticate returns a Backstage Identity - // need to figure out what to return after validating a server token - async validateToken(token: string): Promise { - let maybeUser; - let maybeServer; - try { - maybeUser = await this.identityClient.authenticate(token); - } catch (error) { - // invalid token - } - - try { - if (!this.key) { - throw new Error('No server token defined in config'); - } - maybeServer = JWT.verify(token, this.key); - } catch (error) { - // invalid token - } - - if (!maybeUser && !maybeServer) { - throw new Error(`Invalid token`); - } - return; - } -} diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts new file mode 100644 index 0000000000..88ef1f27c2 --- /dev/null +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/core-app-api'; +import { ServerTokenManager } from './ServerTokenManager'; + +const emptyConfig = new ConfigReader({}); +const configWithSecret = new ConfigReader({ + backend: { authorization: { secret: 'a-secret-key' } }, +}); + +describe('ServerTokenManager', () => { + describe('getServerToken', () => { + it('should always return a token', async () => { + const tokenManager = new ServerTokenManager(configWithSecret); + expect((await tokenManager.getServerToken()).token).toBeDefined(); + + const emptyTokenManager = new ServerTokenManager(emptyConfig); + expect((await emptyTokenManager.getServerToken()).token).toBeDefined(); + }); + }); + + describe('isServerToken', () => { + it('should return true if token is valid', async () => { + const tokenManager = new ServerTokenManager(configWithSecret); + const { token } = await tokenManager.getServerToken(); + const isServerToken = await tokenManager.isServerToken(token); + expect(isServerToken).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); + }); + }); +}); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.ts b/packages/backend-common/src/tokens/ServerTokenManager.ts new file mode 100644 index 0000000000..6737dd4d8b --- /dev/null +++ b/packages/backend-common/src/tokens/ServerTokenManager.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JWK, JWT } from 'jose'; +import { Config } from '@backstage/config'; +import { TokenManager } from './types'; + +export class ServerTokenManager implements TokenManager { + private key: JWK.OctKey; + + constructor(config: Config) { + const secret = + config.getOptionalString('backend.authorization.secret') ?? 'no-secret'; + this.key = JWK.asKey({ kty: 'oct', k: secret }); + } + + async isServerToken(token: string): Promise { + try { + JWT.verify(token, this.key); + return true; + } catch (e) { + 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/index.ts b/packages/backend-common/src/tokens/index.ts index 107d13ac1d..43ff12e597 100644 --- a/packages/backend-common/src/tokens/index.ts +++ b/packages/backend-common/src/tokens/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { AuthIdentityTokenManager } from './AuthIdentityTokenManager'; +export { ServerTokenManager } from './ServerTokenManager'; export type { TokenManager } from './types'; diff --git a/packages/backend-common/src/tokens/types.ts b/packages/backend-common/src/tokens/types.ts index e7d313e660..278695a448 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 }>; - validateToken: (token: string) => Promise; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 57417ce9c6..171460e026 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,7 +33,7 @@ import { SingleHostDiscovery, UrlReaders, useHotMemoize, - AuthIdentityTokenManager, + ServerTokenManager, IdentityClient, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; @@ -66,7 +66,7 @@ function makeCreateEnv(config: Config) { const root = getRootLogger(); const reader = UrlReaders.default({ logger: root, config }); const discovery = SingleHostDiscovery.fromConfig(config); - const tokenManager = new AuthIdentityTokenManager(discovery, config); + const tokenManager = new ServerTokenManager(config); root.info(`Created UrlReader ${reader}`); @@ -158,7 +158,10 @@ async function main() { IdentityClient.getBearerToken(req.headers.authorization) || req.cookies.token; - await authEnv.tokenManager.validateToken(token); + const isServerToken = await authEnv.tokenManager.isServerToken(token); + if (!isServerToken) { + await identity.authenticate(token); + } if (!req.headers.authorization) { // Authorization header may be forwarded by plugin requests diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 1089497db7..4be9c036b3 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -17,10 +17,10 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { - AuthIdentityTokenManager, PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, UrlReader, } from '@backstage/backend-common'; @@ -31,5 +31,5 @@ export type PluginEnvironment = { config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; - tokenManager: AuthIdentityTokenManager; + tokenManager: TokenManager; }; 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 a85f331a7b..87dc390291 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 @@ -17,7 +17,7 @@ import { DatabaseManager, SingleHostDiscovery, UrlReaders, - AuthIdentityTokenManager, + ServerTokenManager, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import app from './plugins/app'; @@ -38,7 +38,7 @@ function makeCreateEnv(config: Config) { const cacheManager = CacheManager.fromConfig(config); const databaseManager = DatabaseManager.fromConfig(config); - const tokenManager = new AuthIdentityTokenManager(discovery, config); + const tokenManager = new ServerTokenManager(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); diff --git a/packages/create-app/templates/default-app/packages/backend/src/types.ts b/packages/create-app/templates/default-app/packages/backend/src/types.ts index 2aac509609..b1e2e0a1df 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/types.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/types.ts @@ -1,10 +1,10 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { - AuthIdentityTokenManager, PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, UrlReader, } from '@backstage/backend-common'; @@ -15,5 +15,5 @@ export type PluginEnvironment = { config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; - tokenManager: AuthIdentityTokenManager; + tokenManager: TokenManager; };