diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md new file mode 100644 index 0000000000..7490f6096a --- /dev/null +++ b/.changeset/strange-olives-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the package from using moment to Luxon. #4278 diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 48463fdd26..c50789d4c1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -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", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts index b22ab0ecda..8896a0d158 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts @@ -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 () => { diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index c24cac55be..e53102e333 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -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 { 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), })), }; } diff --git a/plugins/auth-backend/src/identity/MemoryKeyStore.ts b/plugins/auth-backend/src/identity/MemoryKeyStore.ts index b35002b146..3dc1d777c9 100644 --- a/plugins/auth-backend/src/identity/MemoryKeyStore.ts +++ b/plugins/auth-backend/src/identity/MemoryKeyStore.ts @@ -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(); async addKey(key: AnyJWK): Promise { this.keys.set(key.kid, { - createdAt: utc(), + createdAt: DateTime.utc().toJSDate(), key: JSON.stringify(key), }); } diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 36622332e0..1c6192d5c5 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -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; 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 { // 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', { diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index 827aba51ab..6e984d41cc 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -52,7 +52,7 @@ export type TokenIssuer = { */ export type StoredKey = { key: AnyJWK; - createdAt: moment.Moment; + createdAt: Date; }; /**