From a4d16bcfcc8f4b2565d95fb47175f6a08046c1ff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 15:47:33 +0100 Subject: [PATCH] backend-common: forward service tokens from request if no token manager is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .../auth/authServiceFactory.ts | 2 + .../src/auth/createLegacyAuthAdapters.test.ts | 52 +++++++++++++++++++ .../src/auth/createLegacyAuthAdapters.ts | 43 ++++++++------- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 78ff97a139..ac5c1f9399 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -39,10 +39,12 @@ export type InternalBackstageCredentials = export function createCredentialsWithServicePrincipal( sub: string, + token?: string, ): InternalBackstageCredentials { return { $$type: '@backstage/BackstageCredentials', version: 'v1', + token, principal: { type: 'service', subject: sub, diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts index 43a503199b..354b4a57dc 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockServices } from '@backstage/backend-test-utils'; import { createLegacyAuthAdapters } from './createLegacyAuthAdapters'; +import { Request } from 'express'; describe('createLegacyAuthAdapters', () => { it('should pass through auth if only auth is provided', () => { @@ -86,4 +88,54 @@ describe('createLegacyAuthAdapters', () => { userInfo: expect.any(Object), }); }); + + it('should forward tokens if no token manager is provided', async () => { + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: undefined, + httpAuth: undefined, + discovery: {} as any, + identity: mockServices.identity(), + }); + + const credentials = await httpAuth.credentials({ + headers: { + authorization: 'Bearer my-token', + }, + } as Request); + + await expect( + auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'test', + }), + ).resolves.toEqual({ token: 'my-token' }); + }); + + it('should issue a new token if a token manager is provided', async () => { + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: undefined, + httpAuth: undefined, + tokenManager: { + ...mockServices.tokenManager(), + async getToken() { + return { token: 'new-token' }; + }, + }, + discovery: {} as any, + identity: mockServices.identity(), + }); + + const credentials = await httpAuth.credentials({ + headers: { + authorization: 'Bearer mock-token', + }, + } as Request); + + await expect( + auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'test', + }), + ).resolves.toEqual({ token: 'new-token' }); + }); }); diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index a46d553c5d..64dcc81bc6 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -27,7 +27,7 @@ import { TokenManagerService, UserInfoService, } from '@backstage/backend-plugin-api'; -import { ServerTokenManager, TokenManager } from '../tokens'; +import { TokenManager } from '../tokens'; import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import type { Request, Response } from 'express'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -48,7 +48,7 @@ import { PluginEndpointDiscovery } from '../discovery'; class AuthCompat implements AuthService { constructor( private readonly identity: IdentityService, - private readonly tokenManager: TokenManagerService, + private readonly tokenManager?: TokenManagerService, ) {} isPrincipal( @@ -79,9 +79,12 @@ class AuthCompat implements AuthService { } async authenticate(token: string): Promise { - const { aud } = decodeJwt(token); + // Defensively check whether it seems token-like first, just to support + // custom TokenManager implementations that don't emit JWTs specifically. + const payload = + token.split('.').length === 3 ? decodeJwt(token) : undefined; - if (aud === 'backstage') { + if (payload?.aud === 'backstage') { // User Backstage token const identity = await this.identity.getIdentity({ request: { @@ -100,9 +103,12 @@ class AuthCompat implements AuthService { ); } - await this.tokenManager.authenticate(token); + await this.tokenManager?.authenticate(token); - return createCredentialsWithServicePrincipal('external:backstage-plugin'); + return createCredentialsWithServicePrincipal( + 'external:backstage-plugin', + token, + ); } async getPluginRequestToken(options: { @@ -114,8 +120,12 @@ class AuthCompat implements AuthService { switch (type) { // TODO: Check whether the principal is ourselves - case 'service': - return this.tokenManager.getToken(); + case 'service': { + if (this.tokenManager) { + return this.tokenManager.getToken(); + } + return { token: internalForward.token ?? '' }; + } case 'user': if (!internalForward.token) { throw new Error('User credentials is unexpectedly missing token'); @@ -187,17 +197,13 @@ class HttpAuthCompat implements HttpAuthService { async #extractCredentialsFromRequest(req: Request) { const token = getTokenFromRequest(req); if (!token) { - return createCredentialsWithNonePrincipal(); + return this.#auth.getNoneCredentials(); } - const credentials = toInternalBackstageCredentials( - await this.#auth.authenticate(token), - ); - - return credentials; + return this.#auth.authenticate(token); } - async #getCredentials(req: /* */ RequestWithCredentials) { + async #getCredentials(req: RequestWithCredentials) { return (req[credentialsSymbol] ??= this.#extractCredentialsFromRequest(req)); } @@ -209,9 +215,7 @@ class HttpAuthCompat implements HttpAuthService { allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = toInternalBackstageCredentials( - await this.#getCredentials(req), - ); + const credentials = await this.#getCredentials(req); const allowed = options?.allow; if (!allowed) { @@ -335,9 +339,8 @@ export function createLegacyAuthAdapters< const identity = options.identity ?? DefaultIdentityClient.create({ discovery }); - const tokenManager = options.tokenManager ?? ServerTokenManager.noop(); - const authImpl = new AuthCompat(identity, tokenManager); + const authImpl = new AuthCompat(identity, options.tokenManager); const httpAuthImpl = new HttpAuthCompat(authImpl);