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 27dc3d1200..078b46d3be 100644 --- a/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/UserTokenHandler.ts @@ -17,7 +17,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { AuthenticationError } from '@backstage/errors'; -import { TokenTypes } from '@backstage/plugin-auth-node'; +import { tokenTypes } from '@backstage/plugin-auth-node'; import { base64url, createRemoteJWKSet, @@ -82,18 +82,19 @@ export class UserTokenHandler { #getTokenVerificationOptions(token: string): JWTVerifyOptions | undefined { const { typ } = decodeProtectedHeader(token); - if (typ === TokenTypes.user.typParam) { - return { - algorithms: this.#algorithms, - typ: TokenTypes.user.typParam, - }; - } - - if (typ === TokenTypes.limitedUser.typParam) { + if (typ === tokenTypes.user.typParam) { return { algorithms: this.#algorithms, requiredClaims: ['iat', 'exp', 'sub'], - typ: TokenTypes.limitedUser.typParam, + typ: tokenTypes.user.typParam, + }; + } + + if (typ === tokenTypes.limitedUser.typParam) { + return { + algorithms: this.#algorithms, + requiredClaims: ['iat', 'exp', 'sub'], + typ: tokenTypes.limitedUser.typParam, }; } @@ -112,6 +113,8 @@ export class UserTokenHandler { new TextDecoder().decode(base64url.decode(payloadRaw)), ); + // NOTE: The order and properties in both the header and payload must match + // the usage in plugins/auth-backend/src/identity/TokenFactory.ts const limitedUserToken = [ base64url.encode( JSON.stringify({ diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 6e97742a88..0994e44f68 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -24,11 +24,7 @@ import { } from 'jose'; import { MemoryKeyStore } from './MemoryKeyStore'; import { TokenFactory } from './TokenFactory'; -import { - BackstageUserIdentityProofPayload, - BackstageTokenPayload, - TokenTypes, -} from '@backstage/plugin-auth-node'; +import { tokenTypes } from '@backstage/plugin-auth-node'; const logger = getVoidLogger(); @@ -69,14 +65,11 @@ describe('TokenFactory', () => { const { keys } = await factory.listPublicKeys(); const keyStore = createLocalJWKSet({ keys: keys }); - const verifyResult = await jwtVerify( - token, - keyStore, - ); + const verifyResult = await jwtVerify(token, keyStore); + expect(verifyResult.protectedHeader.typ).toBe(tokenTypes.user.typParam); expect(verifyResult.payload).toEqual({ - typ: TokenTypes.user.typClaim, iss: 'my-issuer', - aud: TokenTypes.user.audClaim, + aud: tokenTypes.user.audClaim, sub: entityRef, ent: [entityRef], 'x-fancy-claim': 'my special claim', @@ -92,14 +85,15 @@ describe('TokenFactory', () => { const limitedUserToken = [ base64url.encode( JSON.stringify({ + typ: tokenTypes.limitedUser.typParam, alg: verifyResult.protectedHeader.alg, kid: verifyResult.protectedHeader.kid!, }), ), base64url.encode( JSON.stringify({ - typ: TokenTypes.limitedUser.typClaim, sub: verifyResult.payload.sub, + ent: verifyResult.payload.ent, iat: verifyResult.payload.iat, exp: verifyResult.payload.exp, }), @@ -107,14 +101,13 @@ describe('TokenFactory', () => { verifyResult.payload.uip, ].join('.'); - const verifyProofResult = - await jwtVerify( - limitedUserToken, - keyStore, - ); + const verifyProofResult = await jwtVerify(limitedUserToken, keyStore); + expect(verifyProofResult.protectedHeader.typ).toBe( + tokenTypes.limitedUser.typParam, + ); expect(verifyProofResult.payload).toEqual({ - typ: TokenTypes.limitedUser.typClaim, sub: entityRef, + ent: [entityRef], iat: expect.any(Number), exp: expect.any(Number), }); diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 1e440a5695..9277361679 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -28,17 +28,87 @@ import { import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { - BackstageTokenPayload, - BackstageUserIdentityProofPayload, - TokenParams, - TokenTypes, -} from '@backstage/plugin-auth-node'; +import { TokenParams, tokenTypes } from '@backstage/plugin-auth-node'; import { AnyJWK, KeyStore, TokenIssuer } from './types'; +import { JsonValue } from '@backstage/types'; const MS_IN_S = 1000; const MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities +/** + * The payload contents of a valid Backstage JWT token + * + * @internal + */ +interface BackstageTokenPayload { + /** + * The issuer of the token, currently the discovery URL of the auth backend + */ + iss: string; + + /** + * The entity ref of the user + */ + sub: string; + + /** + * The entity refs that the user claims ownership througg + */ + ent: string[]; + + /** + * A hard coded audience string + */ + aud: typeof tokenTypes.user.audClaim; + + /** + * Standard expiry in epoch seconds + */ + exp: number; + + /** + * Standard issue time in epoch seconds + */ + iat: number; + + /** + * A separate user identity proof that the auth service can convert to a limited user token + */ + uip: string; + + /** + * Any other custom claims that the adopter may have added + */ + [claim: string]: JsonValue; +} + +/** + * The payload contents of a valid Backstage user identity claim token + * + * @internal + */ +interface BackstageUserIdentityProofPayload { + /** + * The entity ref of the user + */ + sub: string; + + /** + * The ownership entity refs of the user + */ + ent?: string[]; + + /** + * Standard expiry in epoch seconds + */ + exp: number; + + /** + * Standard issue time in epoch seconds + */ + iat: number; +} + type Options = { logger: LoggerService; /** Value of the issuer claim in issued tokens */ @@ -93,7 +163,7 @@ export class TokenFactory implements TokenIssuer { const iss = this.issuer; const { sub, ent = [sub], ...additionalClaims } = params.claims; - const aud = TokenTypes.user.audClaim; + const aud = tokenTypes.user.audClaim; const iat = Math.floor(Date.now() / MS_IN_S); const exp = iat + this.keyDurationSeconds; @@ -116,7 +186,7 @@ export class TokenFactory implements TokenIssuer { const uip = await this.createUserIdentityClaim({ header: { - typ: TokenTypes.limitedUser.typParam, + typ: tokenTypes.limitedUser.typParam, alg: key.alg, kid: key.kid, }, @@ -137,7 +207,7 @@ export class TokenFactory implements TokenIssuer { const token = await new SignJWT(claims) .setProtectedHeader({ - typ: TokenTypes.user.typParam, + typ: tokenTypes.user.typParam, alg: key.alg, kid: key.kid, }) diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index ffa0ef94f8..f9d16c4c3a 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -109,19 +109,6 @@ export interface BackstageSignInResult { token: string; } -// @public -export interface BackstageTokenPayload { - [claim: string]: JsonValue; - aud: typeof TokenTypes.user.audClaim; - ent: string[]; - exp: number; - iat: number; - iss: string; - sub: string; - typ: typeof TokenTypes.user.typClaim; - uip: string; -} - // @public export type BackstageUserIdentity = { type: 'user'; @@ -129,14 +116,6 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; -// @public -export interface BackstageUserIdentityProofPayload { - exp: number; - iat: number; - sub: string; - typ: typeof TokenTypes.limitedUser.typClaim; -} - // @public (undocumented) export type ClientAuthResponse = { providerInfo: TProviderInfo; @@ -661,16 +640,16 @@ export type TokenParams = { }; // @public -export const TokenTypes: Readonly<{ +export const tokenTypes: Readonly<{ user: Readonly<{ - typClaim: 'vnd.backstage.user'; + typParam: 'vnd.backstage.user'; audClaim: 'backstage'; }>; limitedUser: Readonly<{ - typClaim: 'vnd.backstage.limited-user'; + typParam: 'vnd.backstage.limited-user'; }>; service: Readonly<{ - typClaim: 'vnd.backstage.service'; + typParam: 'vnd.backstage.service'; }>; }>; diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index 447e6dc715..8b35aaa1e1 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -35,9 +35,7 @@ export type { AuthResolverContext, BackstageIdentityResponse, BackstageSignInResult, - BackstageTokenPayload, BackstageUserIdentity, - BackstageUserIdentityProofPayload, ClientAuthResponse, CookieConfigurer, ProfileInfo, @@ -46,4 +44,4 @@ export type { SignInResolver, TokenParams, } from './types'; -export { TokenTypes } from './types'; +export { tokenTypes } from './types'; diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index 8fdb91845d..8cc48aacc4 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -381,7 +381,7 @@ export type CookieConfigurer = (ctx: { * * @public */ -export const TokenTypes = Object.freeze({ +export const tokenTypes = Object.freeze({ user: Object.freeze({ typParam: 'vnd.backstage.user', audClaim: 'backstage', @@ -393,77 +393,3 @@ export const TokenTypes = Object.freeze({ typParam: 'vnd.backstage.service', }), }); - -/** - * The payload contents of a valid Backstage JWT token - * - * @public - */ -export interface BackstageTokenPayload { - /** - * The issuer of the token, currently the discovery URL of the auth backend - */ - iss: string; - - /** - * The entity ref of the user - */ - sub: string; - - /** - * The entity refs that the user claims ownership througg - */ - ent: string[]; - - /** - * A hard coded audience string - */ - aud: typeof TokenTypes.user.audClaim; - - /** - * Standard expiry in epoch seconds - */ - exp: number; - - /** - * Standard issue time in epoch seconds - */ - iat: number; - - /** - * A separate user identity proof that the auth service can convert to a limited user token - */ - uip: string; - - /** - * Any other custom claims that the adopter may have added - */ - [claim: string]: JsonValue; -} - -/** - * The payload contents of a valid Backstage user identity claim token - * - * @public - */ -export interface BackstageUserIdentityProofPayload { - /** - * The entity ref of the user - */ - sub: string; - - /** - * The ownership entity refs of the user - */ - ent?: string[]; - - /** - * Standard expiry in epoch seconds - */ - exp: number; - - /** - * Standard issue time in epoch seconds - */ - iat: number; -}