auth-backend: more docs for TokenFactory
This commit is contained in:
@@ -31,6 +31,20 @@ type Options = {
|
||||
keyDuration: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A token issuer that is able to issue tokens in a distributed system
|
||||
* backed by a single database. Tokens are issued using lazily generated
|
||||
* signing keys, where each running instance of the auth service uses its own
|
||||
* signing key.
|
||||
*
|
||||
* The public parts of the keys are all stored in the shared key storage,
|
||||
* and any of the instances of the auth service will return the full list
|
||||
* of public keys that are currently in storage.
|
||||
*
|
||||
* Signing keys are automatically rotated at the same interval as the token
|
||||
* duration. Expired keys are kept in storage until there are no valid tokens
|
||||
* in circulation that could have been signed by that key.
|
||||
*/
|
||||
export class TokenFactory implements TokenIssuer {
|
||||
private readonly issuer: string;
|
||||
private readonly logger: Logger;
|
||||
@@ -64,6 +78,9 @@ export class TokenFactory implements TokenIssuer {
|
||||
});
|
||||
}
|
||||
|
||||
// This will be called by other services that want to verify ID tokens.
|
||||
// It is important that it returns a list of all public keys that could
|
||||
// have been used to sign tokens that have not yet expired.
|
||||
async listPublicKeys(): Promise<{ keys: AnyJWK[] }> {
|
||||
const { items: keys } = await this.keyStore.listKeys();
|
||||
|
||||
@@ -80,6 +97,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
}
|
||||
}
|
||||
|
||||
// Lazily prune expired keys. This may cause duplicate removals if we have concurrent callers, but w/e
|
||||
if (expiredKeys.length > 0) {
|
||||
const kids = expiredKeys.map(({ key }) => key.kid);
|
||||
|
||||
@@ -96,6 +114,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
}
|
||||
|
||||
private async getKey(): Promise<JSONWebKey> {
|
||||
// Make sure that we only generate one key at a time
|
||||
if (this.privateKeyPromise) {
|
||||
if (this.keyExpiry && Date.now() < this.keyExpiry) {
|
||||
return this.privateKeyPromise;
|
||||
@@ -106,15 +125,23 @@ export class TokenFactory implements TokenIssuer {
|
||||
|
||||
this.keyExpiry = Date.now() + this.keyDuration * MS_IN_S;
|
||||
const promise = (async () => {
|
||||
// This generates a new signing key to be used to sign tokens until the next key rotation
|
||||
const key = await JWK.generate('EC', 'P-256', {
|
||||
use: 'sig',
|
||||
kid: uuid(),
|
||||
alg: 'ES256',
|
||||
});
|
||||
|
||||
// We're not allowed to use the key until it has been successfully stored
|
||||
// TODO: some token verification implementations aggressively cache the list of keys, and
|
||||
// don't attempt to fetch new ones even if they encounter an unknown kid. Therefore we
|
||||
// may want to keep using the existing key for some period of time until we switch to
|
||||
// the new one. This also needs to be implemented cross-service though, meaning new services
|
||||
// that boot up need to be able to grab an existing key to use for signing.
|
||||
this.logger.info(`Created new signing key ${key.kid}`);
|
||||
await this.keyStore.addKey((key.toJWK(false) as unknown) as AnyJWK);
|
||||
|
||||
// At this point we are allowed to start using the new key
|
||||
return key as JSONWebKey;
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user