add caching as well

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-05-04 15:26:41 +02:00
parent 5fcbd86960
commit 4874ebc2af
2 changed files with 50 additions and 17 deletions
@@ -169,26 +169,37 @@ describe('ServerTokenManager', () => {
);
});
it('should throw for expired tokens', async () => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2020-02-02T02:00:00.0000000Z'));
it('should throw for expired tokens, and re-issue new ones', async () => {
jest.useFakeTimers();
const tokenManager = ServerTokenManager.fromConfig(configWithSecret, {
logger,
});
const { token } = await tokenManager.getToken();
await expect(tokenManager.authenticate(token)).resolves.not.toThrow();
const { token: token1 } = await tokenManager.getToken();
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
jest.setSystemTime(new Date('2020-02-02T02:59:00.0000000Z'));
await expect(tokenManager.authenticate(token)).resolves.not.toThrow();
// Less than ten minutes before expiry, it still returns the same token
jest.advanceTimersByTime(49 * 60 * 1000);
const { token: token1Again } = await tokenManager.getToken();
expect(token1).toEqual(token1Again);
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
jest.setSystemTime(new Date('2020-02-02T03:00:01.0000000Z'));
// Right before the expiry, the old ones are still valid but returning a new token
jest.advanceTimersByTime(10 * 60 * 1000);
const { token: token2 } = await tokenManager.getToken();
expect(token1).not.toEqual(token2);
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
// After expiry, the newest one is still valid
jest.advanceTimersByTime(2 * 60 * 1000);
await expect(
tokenManager.authenticate(token),
tokenManager.authenticate(token1),
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Invalid server token: JWTExpired: \\"exp\\" claim timestamp check failed"',
);
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
});
});
@@ -17,11 +17,13 @@
import { Config } from '@backstage/config';
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
import { base64url, exportJWK, generateSecret, jwtVerify, SignJWT } from 'jose';
import { DateTime, Duration } from 'luxon';
import { Logger } from 'winston';
import { TokenManager } from './types';
const TOKEN_ALG = 'HS256';
const TOKEN_SUB = 'backstage-server';
const TOKEN_EXPIRY = Duration.fromObject({ hours: 1 });
/**
* A token manager that issues static dummy tokens and never fails
@@ -57,9 +59,10 @@ export interface ServerTokenManagerOptions {
*/
export class ServerTokenManager implements TokenManager {
private readonly options: ServerTokenManagerOptions;
private verificationKeys: Uint8Array[];
private readonly verificationKeys: Uint8Array[];
private signingKey: Uint8Array;
private privateKeyPromise?: Promise<void>;
private privateKeyPromise: Promise<void> | undefined;
private currentTokenPromise: Promise<{ token: string }> | undefined;
/**
* Creates a token manager that issues static dummy tokens and never fails
@@ -140,13 +143,32 @@ export class ServerTokenManager implements TokenManager {
await this.generateKeys();
}
const jwt = await new SignJWT({})
.setProtectedHeader({ alg: TOKEN_ALG })
.setSubject(TOKEN_SUB)
.setExpirationTime('1h')
.sign(this.signingKey);
if (this.currentTokenPromise) {
return this.currentTokenPromise;
}
return { token: jwt };
const result = Promise.resolve().then(async () => {
const jwt = await new SignJWT({})
.setProtectedHeader({ alg: TOKEN_ALG })
.setSubject(TOKEN_SUB)
.setExpirationTime(DateTime.now().plus(TOKEN_EXPIRY).toUnixInteger())
.sign(this.signingKey);
return { token: jwt };
});
this.currentTokenPromise = result;
result
.then(() => {
setTimeout(() => {
this.currentTokenPromise = undefined;
}, TOKEN_EXPIRY.minus({ minutes: 5 }).toMillis());
})
.catch(() => {
this.currentTokenPromise = undefined;
});
return result;
}
async authenticate(token: string): Promise<void> {