From 22c0bd2adfef3b598ec88d3733daa1cc116ae4c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 2 Apr 2024 18:01:43 +0200 Subject: [PATCH] backend-app-api: refactor auth service implementation to use UserTokenHandler more Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .../implementations/auth/UserTokenHandler.ts | 64 +++++++++---------- .../auth/authServiceFactory.ts | 48 ++++---------- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts b/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts index dbada2f1a7..27dc3d1200 100644 --- a/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts @@ -26,6 +26,7 @@ import { FlattenedJWSInput, JWSHeaderParameters, jwtVerify, + JWTVerifyOptions, } from 'jose'; import { GetKeyFunction } from 'jose/dist/types/types'; @@ -49,21 +50,23 @@ export class UserTokenHandler { this.#algorithms = ['ES256']; // TODO: configurable? } - async verifyFullUserToken(token: string): Promise<{ userEntityRef: string }> { - // Check if the keystore needs to be updated + async verifyToken(token: string) { + const verifyOpts = this.#getTokenVerificationOptions(token); + if (!verifyOpts) { + return undefined; + } + await this.refreshKeyStore(token); if (!this.#keyStore) { throw new AuthenticationError('No keystore exists'); } - // Verify token claims and signature - // Note: Claims must match those set by TokenFactory when issuing tokens - // Note: verify throws if verification fails - const { payload } = await jwtVerify(token, this.#keyStore, { - algorithms: this.#algorithms, - audience: 'backstage', - }).catch(e => { - console.log(`DEBUG: e=`, e); + // Verify a limited token, ensuring the necessarily claims are present and token type is correct + const { payload } = await jwtVerify( + token, + this.#keyStore, + verifyOpts, + ).catch(e => { throw new AuthenticationError('invalid token', e); }); @@ -76,31 +79,28 @@ export class UserTokenHandler { return { userEntityRef }; } - async verifyLimitedUserToken( - token: string, - ): Promise<{ userEntityRef: string }> { - await this.refreshKeyStore(token); - if (!this.#keyStore) { - throw new AuthenticationError('No keystore exists'); + #getTokenVerificationOptions(token: string): JWTVerifyOptions | undefined { + const { typ } = decodeProtectedHeader(token); + + if (typ === TokenTypes.user.typParam) { + return { + algorithms: this.#algorithms, + typ: TokenTypes.user.typParam, + }; } - // Verify a limited token, ensuring the necessarily claims are present and token type is correct - const { payload } = await jwtVerify(token, this.#keyStore, { + if (typ === TokenTypes.limitedUser.typParam) { + return { + algorithms: this.#algorithms, + requiredClaims: ['iat', 'exp', 'sub'], + typ: TokenTypes.limitedUser.typParam, + }; + } + + return { algorithms: this.#algorithms, - requiredClaims: ['iat', 'exp', 'sub'], - typ: TokenTypes.limitedUser.typParam, - }).catch(e => { - console.log(`DEBUG: e=`, e); - throw new AuthenticationError('invalid token', e); - }); - - const userEntityRef = payload.sub; - - if (!userEntityRef) { - throw new AuthenticationError('No user sub found in token'); - } - - return { userEntityRef }; + audience: 'backstage', + }; } createLimitedUserToken(backstageToken: string) { 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 41b8a12a3a..d39c57d328 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -26,8 +26,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { AuthenticationError } from '@backstage/errors'; -import { TokenTypes } from '@backstage/plugin-auth-node'; -import { decodeJwt, decodeProtectedHeader } from 'jose'; +import { decodeJwt } from 'jose'; import { UserTokenHandler } from './UserTokenHandler'; /** @internal */ @@ -112,49 +111,24 @@ class DefaultAuthService implements AuthService { // allowLimitedAccess is currently ignored, since we currently always use the full user tokens async authenticate(token: string): Promise { - const { typ } = decodeProtectedHeader(token); const { sub, aud } = decodeJwt(token); - if (typ) { - switch (typ) { - case TokenTypes.user.typParam: { - const { userEntityRef } = - await this.userTokenHandler.verifyFullUserToken(token); - return createCredentialsWithUserPrincipal( - userEntityRef, - token, - this.#getJwtExpiration(token), - ); - } - case TokenTypes.limitedUser.typParam: { - const { userEntityRef } = - await this.userTokenHandler.verifyLimitedUserToken(token); - return createCredentialsWithUserPrincipal( - userEntityRef, - token, - this.#getJwtExpiration(token), - ); - } - default: - throw new AuthenticationError("Invalid token 'typ' claim"); - } - } - // Legacy service-to-service token if (sub === 'backstage-server' && !aud) { await this.tokenManager.authenticate(token); return createCredentialsWithServicePrincipal('external:backstage-plugin'); } - // User Backstage token - const { userEntityRef } = await this.userTokenHandler.verifyFullUserToken( - token, - ); - return createCredentialsWithUserPrincipal( - userEntityRef, - token, - this.#getJwtExpiration(token), - ); + const userResult = await this.userTokenHandler.verifyToken(token); + if (userResult) { + return createCredentialsWithUserPrincipal( + userResult.userEntityRef, + token, + this.#getJwtExpiration(token), + ); + } + + throw new AuthenticationError('Unknown token'); } isPrincipal(