Merge pull request #4386 from backstage/auth-backend-port-to-luxon

Auth backend port to Luxon
This commit is contained in:
Nils Streijffert
2021-02-11 14:22:30 +01:00
committed by GitHub
7 changed files with 49 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Migrated the package from using moment to Luxon. #4278
+1 -1
View File
@@ -47,7 +47,7 @@
"jose": "^1.27.1",
"jwt-decode": "^3.1.0",
"knex": "^0.21.6",
"moment": "^2.26.0",
"luxon": "^1.25.0",
"morgan": "^1.10.0",
"node-cache": "^5.1.2",
"openid-client": "^4.2.1",
@@ -15,8 +15,8 @@
*/
import Knex from 'knex';
import moment from 'moment';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DateTime } from 'luxon';
function createDB() {
const knex = Knex({
@@ -51,7 +51,11 @@ describe('DatabaseKeyStore', () => {
const { items } = await store.listKeys();
expect(items).toEqual([{ createdAt: expect.anything(), key }]);
expect(Math.abs(items[0].createdAt.diff(moment(), 's'))).toBeLessThan(10);
expect(
Math.abs(
DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
),
).toBeLessThan(10);
});
it('should remove stored keys', async () => {
@@ -15,9 +15,9 @@
*/
import Knex from 'knex';
import { utc } from 'moment';
import { resolvePackagePath } from '@backstage/backend-common';
import { AnyJWK, KeyStore, StoredKey } from './types';
import { DateTime } from 'luxon';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-auth-backend',
@@ -27,7 +27,7 @@ const migrationsDir = resolvePackagePath(
const TABLE = 'signing_keys';
type Row = {
created_at: Date;
created_at: Date; // row.created_at is a string after being returned from the database
kid: string;
key: string;
};
@@ -36,6 +36,21 @@ type Options = {
database: Knex;
};
const parseDate = (date: string | Date) => {
const parsedDate =
typeof date === 'string'
? DateTime.fromSQL(date, { locale: 'UTC' })
: DateTime.fromJSDate(date);
if (!parsedDate.isValid) {
throw new Error(
`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`,
);
}
return parsedDate.toJSDate();
};
export class DatabaseKeyStore implements KeyStore {
static async create(options: Options): Promise<DatabaseKeyStore> {
const { database } = options;
@@ -66,7 +81,7 @@ export class DatabaseKeyStore implements KeyStore {
return {
items: rows.map(row => ({
key: JSON.parse(row.key),
createdAt: utc(row.created_at),
createdAt: parseDate(row.created_at),
})),
};
}
@@ -14,18 +14,15 @@
* limitations under the License.
*/
import { utc } from 'moment';
import { KeyStore, AnyJWK, StoredKey } from './types';
import { DateTime } from 'luxon';
export class MemoryKeyStore implements KeyStore {
private readonly keys = new Map<
string,
{ createdAt: moment.Moment; key: string }
>();
private readonly keys = new Map<string, { createdAt: Date; key: string }>();
async addKey(key: AnyJWK): Promise<void> {
this.keys.set(key.kid, {
createdAt: utc(),
createdAt: DateTime.utc().toJSDate(),
key: JSON.stringify(key),
});
}
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import moment from 'moment';
import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types';
import { JSONWebKey, JWK, JWS } from 'jose';
import { Logger } from 'winston';
import { v4 as uuid } from 'uuid';
import { DateTime } from 'luxon';
const MS_IN_S = 1000;
@@ -52,7 +52,7 @@ export class TokenFactory implements TokenIssuer {
private readonly keyStore: KeyStore;
private readonly keyDurationSeconds: number;
private keyExpiry?: moment.Moment;
private keyExpiry?: Date;
private privateKeyPromise?: Promise<JSONWebKey>;
constructor(options: Options) {
@@ -90,8 +90,10 @@ export class TokenFactory implements TokenIssuer {
for (const key of keys) {
// Allow for a grace period of another full key duration before we remove the keys from the database
const expireAt = key.createdAt.add(3 * this.keyDurationSeconds, 's');
if (expireAt.isBefore()) {
const expireAt = DateTime.fromJSDate(key.createdAt).plus({
seconds: 3 * this.keyDurationSeconds,
});
if (expireAt < DateTime.local()) {
expiredKeys.push(key);
} else {
validKeys.push(key);
@@ -117,14 +119,21 @@ 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?.isAfter()) {
if (
this.keyExpiry &&
DateTime.fromJSDate(this.keyExpiry) > DateTime.local()
) {
return this.privateKeyPromise;
}
this.logger.info(`Signing key has expired, generating new key`);
delete this.privateKeyPromise;
}
this.keyExpiry = moment().add(this.keyDurationSeconds, 'seconds');
this.keyExpiry = DateTime.utc()
.plus({
seconds: this.keyDurationSeconds,
})
.toJSDate();
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', {
+1 -1
View File
@@ -52,7 +52,7 @@ export type TokenIssuer = {
*/
export type StoredKey = {
key: AnyJWK;
createdAt: moment.Moment;
createdAt: Date;
};
/**