plugin-auth-backend: Added validation to ensure any custom auth resolvers are using EntityRefs for subject claims
Signed-off-by: Harry Hogg <hhogg@spotify.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': minor
|
||||
---
|
||||
|
||||
Added validation to TokenFactory.issueToken that ensure any sub claim given is a valid entityRef. This will affect any custom resolver functions given to auth providers.
|
||||
@@ -18,6 +18,7 @@ import { MemoryKeyStore } from './MemoryKeyStore';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { JWKS, JSONWebKey, JWT } from 'jose';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -28,6 +29,12 @@ function jwtKid(jwt: string): string {
|
||||
return header.kid;
|
||||
}
|
||||
|
||||
const entityRef = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
namespace: 'default',
|
||||
name: 'JackFrost',
|
||||
});
|
||||
|
||||
describe('TokenFactory', () => {
|
||||
it('should issue valid tokens signed by a listed key', async () => {
|
||||
const keyDurationSeconds = 5;
|
||||
@@ -39,7 +46,7 @@ describe('TokenFactory', () => {
|
||||
});
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] });
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token = await factory.issueToken({ claims: { sub: entityRef } });
|
||||
|
||||
const { keys } = await factory.listPublicKeys();
|
||||
const keyStore = JWKS.asKeyStore({
|
||||
@@ -53,7 +60,7 @@ describe('TokenFactory', () => {
|
||||
expect(payload).toEqual({
|
||||
iss: 'my-issuer',
|
||||
aud: 'backstage',
|
||||
sub: 'foo',
|
||||
sub: entityRef,
|
||||
iat: expect.any(Number),
|
||||
exp: expect.any(Number),
|
||||
});
|
||||
@@ -71,8 +78,12 @@ describe('TokenFactory', () => {
|
||||
logger,
|
||||
});
|
||||
|
||||
const token1 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token2 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token1 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
const token2 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
expect(jwtKid(token1)).toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
@@ -89,7 +100,9 @@ describe('TokenFactory', () => {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const token3 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token3 = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
});
|
||||
expect(jwtKid(token3)).not.toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
@@ -100,4 +113,20 @@ describe('TokenFactory', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error with a non entityRef sub claim', async () => {
|
||||
const keyDurationSeconds = 5;
|
||||
const factory = new TokenFactory({
|
||||
issuer: 'my-issuer',
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
return factory.issueToken({
|
||||
claims: { sub: 'UserId' },
|
||||
});
|
||||
}).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { JSONWebKey, JWK, JWS } from 'jose';
|
||||
import { Logger } from 'winston';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DateTime } from 'luxon';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const MS_IN_S = 1000;
|
||||
|
||||
@@ -72,6 +73,15 @@ export class TokenFactory implements TokenIssuer {
|
||||
const iat = Math.floor(Date.now() / MS_IN_S);
|
||||
const exp = iat + this.keyDurationSeconds;
|
||||
|
||||
// Validate that the subject claim is a valid EntityRef
|
||||
try {
|
||||
parseEntityRef(sub);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'"sub" claim provided by the auth resolver is not a valid EntityRef.',
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`);
|
||||
|
||||
return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
||||
|
||||
Reference in New Issue
Block a user