auth-backend: more docs for TokenFactory

This commit is contained in:
Patrik Oldsberg
2020-06-20 17:27:20 +02:00
parent 8e05f91c29
commit 20cb44ff2c
@@ -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;
})();