diff --git a/.changeset/cold-bananas-hang.md b/.changeset/cold-bananas-hang.md new file mode 100644 index 0000000000..8e535a5532 --- /dev/null +++ b/.changeset/cold-bananas-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Allow adding misc claims to JWT diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index d47abeee83..6e9de12cdb 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -706,7 +706,7 @@ export type TokenParams = { claims: { sub: string; ent?: string[]; - }; + } & Record; }; // @public (undocumented) diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 9a163b3ff5..4f6df89210 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -48,7 +48,12 @@ describe('TokenFactory', () => { await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] }); const token = await factory.issueToken({ - claims: { sub: entityRef, ent: [entityRef] }, + claims: { + sub: entityRef, + ent: [entityRef], + 'x-fancy-claim': 'my special claim', + aud: 'this value will be overridden', + }, }); const { keys } = await factory.listPublicKeys(); @@ -60,6 +65,7 @@ describe('TokenFactory', () => { aud: 'backstage', sub: entityRef, ent: [entityRef], + 'x-fancy-claim': 'my special claim', 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 44cfebfb71..f80b79b6bc 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -77,8 +77,7 @@ export class TokenFactory implements TokenIssuer { const key = await this.getKey(); const iss = this.issuer; - const sub = params.claims.sub; - const ent = params.claims.ent; + const { sub, ent, ...additionalClaims } = params.claims; const aud = 'backstage'; const iat = Math.floor(Date.now() / MS_IN_S); const exp = iat + this.keyDurationSeconds; @@ -98,7 +97,7 @@ export class TokenFactory implements TokenIssuer { throw new AuthenticationError('No algorithm was provided in the key'); } - return new SignJWT({ iss, sub, ent, aud, iat, exp }) + return new SignJWT({ ...additionalClaims, iss, sub, ent, aud, iat, exp }) .setProtectedHeader({ alg: key.alg, kid: key.kid }) .setIssuer(iss) .setAudience(aud) diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 4765b50d7d..e325f74c81 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { JsonValue } from '@backstage/types'; + /** Represents any form of serializable JWK */ export interface AnyJWK extends Record { use: 'sig'; @@ -28,13 +30,20 @@ export interface AnyJWK extends Record { * @public */ export type TokenParams = { - /** The claims that will be embedded within the token */ + /** + * The claims that will be embedded within the token. At a minimum, this should include + * the subject claim, `sub`. It is common to also list entity ownership relations in the + * `ent` list. Additional claims may also be added at the developer's discretion except + * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`, + * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future + * without listing the change as a "breaking change". + */ claims: { /** The token subject, i.e. User ID */ sub: string; /** A list of entity references that the user claims ownership through */ ent?: string[]; - }; + } & Record; }; /**