auth-backend: make DatabaseKeyStore only return valid keys and remove expired ones

This commit is contained in:
Patrik Oldsberg
2020-06-19 15:24:29 +02:00
parent 90cc2ca615
commit adc574ee33
3 changed files with 58 additions and 4 deletions
@@ -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');
+1
View File
@@ -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",
@@ -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<PublicKey[]> {
const rows = await this.database<Row>(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;
}
}
}