diff --git a/plugins/auth-backend/migrations/20200619125845_init.js b/plugins/auth-backend/migrations/20200619125845_init.js index 295e4eea40..6d06fd1125 100644 --- a/plugins/auth-backend/migrations/20200619125845_init.js +++ b/plugins/auth-backend/migrations/20200619125845_init.js @@ -30,7 +30,7 @@ exports.up = async function up(knex) { .notNullable() .comment('ID of the signing key'); table - .timestamp('created_at') + .timestamp('created_at', { useTz: false, precision: 0 }) .notNullable() .defaultTo(knex.fn.now()) .comment('The creation time of the key'); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index bee3b3c8b5..7da7b75968 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -35,6 +35,7 @@ "jose": "^1.27.1", "jwt-decode": "2.2.0", "knex": "^0.21.1", + "moment": "^2.26.0", "morgan": "^1.10.0", "passport": "^0.4.1", "passport-github2": "^0.1.12", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index d3ea588c14..016761fe99 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -16,6 +16,7 @@ import Knex from 'knex'; import path from 'path'; +import { utc } from 'moment'; import { Logger } from 'winston'; import { PublicKey } from './types'; @@ -24,6 +25,8 @@ const migrationsDir = path.resolve( '../migrations', ); +const KEY_DURATION_MS = 3600 * 1000; + const TABLE = 'signing_keys'; type Row = { @@ -51,6 +54,8 @@ export class DatabaseKeyStore { private readonly logger: Logger; private readonly database: Knex; + private removingExpiredRows: boolean = false; + private constructor(options: Options) { const { logger, database } = options; @@ -65,12 +70,60 @@ export class DatabaseKeyStore { kid: key.kid, key: JSON.stringify(key), }); - console.log(`DEBUG: stored key`); } async listPublicKeys(): Promise { const rows = await this.database(TABLE).select(); - console.log('DEBUG: rows =', rows); - return rows.map(row => JSON.parse(row.key)); + + const [validRows, expiredRows] = this.splitExpiredRows(rows); + if (expiredRows.length > 0) { + // We don't await this, just let it run in the background + this.removeExpiredRows(expiredRows); + } + + return validRows.map(row => JSON.parse(row.key)); + } + + private splitExpiredRows(rows: Row[]) { + const validRows = []; + const expiredRows = []; + + for (const row of rows) { + const createdAt = utc(row.created_at); + const expireAt = createdAt.add(3 * KEY_DURATION_MS, 'ms'); + const isExpired = expireAt.isBefore(); + + if (isExpired) { + expiredRows.push(row); + } else { + validRows.push(row); + } + } + + return [validRows, expiredRows]; + } + + private async removeExpiredRows(rows: Row[]) { + if (this.removingExpiredRows) { + return; + } + + try { + this.removingExpiredRows = true; + + const kids = rows.map(row => row.kid); + this.logger.info(`Removing expired signing keys, '${kids.join(', ')}'`); + + const result = await this.database(TABLE).delete().whereIn('kid', kids); + if (result !== kids.length) { + this.logger.warn( + `Wanted to remove ${kids.length} expired signing, but removed ${result} instead`, + ); + } + } catch (error) { + this.logger.error(`Failed to remove expired signing keys, ${error}`); + } finally { + this.removingExpiredRows = false; + } } }