@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user