auth-backend: make DatabaseKeyStore only return valid keys and remove expired ones
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user