message: auth-backend: more docs for TokenFactory
diff: @@ -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;
  })();
  