auth-backend: added tests for TokenFactory and fix keyDuration being ignored
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { utc } from 'moment';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { KeyStore, AnyJWK, StoredKey } from './types';
|
||||
import { JWKS, JSONWebKey, JWT } from 'jose';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
class MemoryKeyStore implements KeyStore {
|
||||
private readonly keys = new Map<
|
||||
string,
|
||||
{ createdAt: moment.Moment; key: string }
|
||||
>();
|
||||
|
||||
async addKey(key: AnyJWK): Promise<void> {
|
||||
this.keys.set(key.kid, {
|
||||
createdAt: utc(),
|
||||
key: JSON.stringify(key),
|
||||
});
|
||||
}
|
||||
|
||||
async removeKeys(kids: string[]): Promise<void> {
|
||||
for (const kid of kids) {
|
||||
this.keys.delete(kid);
|
||||
}
|
||||
}
|
||||
|
||||
async listKeys(): Promise<{ items: StoredKey[] }> {
|
||||
return {
|
||||
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
||||
createdAt,
|
||||
key: JSON.parse(keyStr),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function jwtKid(jwt: string): string {
|
||||
const { header } = JWT.decode(jwt, { complete: true }) as {
|
||||
header: { kid: string };
|
||||
};
|
||||
return header.kid;
|
||||
}
|
||||
|
||||
describe('TokenFactory', () => {
|
||||
it('should issue valid tokens signed by a listed key', async () => {
|
||||
const keyDuration = 5;
|
||||
const factory = new TokenFactory({
|
||||
issuer: 'my-issuer',
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDuration,
|
||||
logger,
|
||||
});
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] });
|
||||
const token = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
|
||||
const { keys } = await factory.listPublicKeys();
|
||||
const keyStore = JWKS.asKeyStore({
|
||||
keys: keys.map(key => key as JSONWebKey),
|
||||
});
|
||||
|
||||
const payload = JWT.verify(token, keyStore) as object & {
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
expect(payload).toEqual({
|
||||
iss: 'my-issuer',
|
||||
aud: 'backstage',
|
||||
sub: 'foo',
|
||||
iat: expect.any(Number),
|
||||
exp: expect.any(Number),
|
||||
});
|
||||
expect(payload.exp).toBe(payload.iat + keyDuration * 1000);
|
||||
});
|
||||
|
||||
it('should generate new signing keys when the current one expires', async () => {
|
||||
const fixedTime = Date.now();
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => fixedTime);
|
||||
|
||||
const factory = new TokenFactory({
|
||||
issuer: 'my-issuer',
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDuration: 5,
|
||||
logger,
|
||||
});
|
||||
|
||||
const token1 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
const token2 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
expect(jwtKid(token1)).toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
keys: [
|
||||
expect.objectContaining({
|
||||
kid: jwtKid(token1),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => fixedTime + 60000);
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const token3 = await factory.issueToken({ claims: { sub: 'foo' } });
|
||||
expect(jwtKid(token3)).not.toBe(jwtKid(token2));
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({
|
||||
keys: [
|
||||
expect.objectContaining({
|
||||
kid: jwtKid(token3),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ import { JSONWebKey, JWK, JWS } from 'jose';
|
||||
import { Logger } from 'winston';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const MS_IN_S = 1000;
|
||||
|
||||
type Options = {
|
||||
logger: Logger;
|
||||
/** Value of the issuer claim in issued tokens */
|
||||
@@ -51,8 +53,8 @@ export class TokenFactory implements TokenIssuer {
|
||||
const iss = this.issuer;
|
||||
const sub = params.claims.sub;
|
||||
const aud = 'backstage';
|
||||
const iat = (Date.now() / 1000) | 0;
|
||||
const exp = iat + 3600;
|
||||
const iat = (Date.now() / MS_IN_S) | 0;
|
||||
const exp = iat + this.keyDuration * MS_IN_S;
|
||||
|
||||
this.logger.info(`Issuing token for ${sub}`);
|
||||
|
||||
@@ -102,7 +104,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
delete this.privateKeyPromise;
|
||||
}
|
||||
|
||||
this.keyExpiry = Date.now() + this.keyDuration * 1000;
|
||||
this.keyExpiry = Date.now() + this.keyDuration * MS_IN_S;
|
||||
const promise = (async () => {
|
||||
const key = await JWK.generate('EC', 'P-256', {
|
||||
use: 'sig',
|
||||
|
||||
Reference in New Issue
Block a user