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:
Harry Hogg
2022-03-01 17:46:39 +00:00
parent c89785166d
commit 0c8ba31d72
3 changed files with 49 additions and 5 deletions
+5
View File
@@ -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, {