diff --git a/src/internal/auth/jwks/channels.ts b/src/internal/auth/jwks/channels.ts deleted file mode 100644 index 57c12e255..000000000 --- a/src/internal/auth/jwks/channels.ts +++ /dev/null @@ -1 +0,0 @@ -export const TENANTS_JWKS_UPDATE_CHANNEL = 'tenants_jwks_update' diff --git a/src/internal/auth/jwks/constants.ts b/src/internal/auth/jwks/constants.ts new file mode 100644 index 000000000..9055f519a --- /dev/null +++ b/src/internal/auth/jwks/constants.ts @@ -0,0 +1,4 @@ +export const TENANTS_JWKS_UPDATE_CHANNEL = 'tenants_jwks_update' + +// Reserved `kind` values for a tenant's url-signing key lifecycle. +export const JWK_KIND_STORAGE_URL_SIGNING = 'storage-url-signing-key' diff --git a/src/internal/auth/jwks/kid.ts b/src/internal/auth/jwks/kid.ts new file mode 100644 index 000000000..0c50b92de --- /dev/null +++ b/src/internal/auth/jwks/kid.ts @@ -0,0 +1,15 @@ +import { JWK_KIND_STORAGE_URL_SIGNING } from './constants' + +export const JWK_KID_SEPARATOR = '_' + +const LEGACY_URL_SIGNING_KID_PREFIX = `${JWK_KIND_STORAGE_URL_SIGNING}${JWK_KID_SEPARATOR}` + +/** + * Strips a legacy "_" prefix, but only for the (pre-existing) signing kind - other kids + * (custom addJwk kinds, standby kids, or the current bare-id format) are returned unchanged. + */ +export function normalizeUrlSigningKid(kid: string): string { + return kid.startsWith(LEGACY_URL_SIGNING_KID_PREFIX) + ? kid.slice(LEGACY_URL_SIGNING_KID_PREFIX.length) + : kid +} diff --git a/src/internal/auth/jwks/manager.ts b/src/internal/auth/jwks/manager.ts index c709f1733..b9e1b39d5 100644 --- a/src/internal/auth/jwks/manager.ts +++ b/src/internal/auth/jwks/manager.ts @@ -10,12 +10,10 @@ import { import { createInvalidatableSingleFlightByKey } from '@internal/concurrency' import { isStringMessage, PubSubAdapter } from '@internal/pubsub' import { freezeJwksConfig, JwksConfig, JwksConfigKeyOCT } from '../../../config' -import { TENANTS_JWKS_UPDATE_CHANNEL } from './channels' +import { JWK_KIND_STORAGE_URL_SIGNING, TENANTS_JWKS_UPDATE_CHANNEL } from './constants' +import { JWK_KID_SEPARATOR } from './kid' import { JWKSManagerStore } from './store' -const JWK_KIND_STORAGE_URL_SIGNING = 'storage-url-signing-key' -const JWK_KID_SEPARATOR = '_' - const tenantJwksSingleFlight = createInvalidatableSingleFlightByKey() // Max 16,384 items. At ~2.5KB per JWKS, this uses roughly ~40MB of heap memory worst-case. export const TENANT_JWKS_CACHE_MAX_ITEMS = 16384 diff --git a/src/internal/auth/jwt.ts b/src/internal/auth/jwt.ts index 0b4f518ac..e2a03b5e0 100644 --- a/src/internal/auth/jwt.ts +++ b/src/internal/auth/jwt.ts @@ -20,6 +20,7 @@ import { SignJWT, } from 'jose' import { getConfig, JwksConfig, JwksConfigKey, JwksConfigKeyOCT } from '../../config' +import { normalizeUrlSigningKid } from './jwks/kid' const { jwtAlgorithm } = getConfig() @@ -241,6 +242,20 @@ function getPreparedJWTSigningKey(key: string | JwksConfigKeyOCT, alg: string): ) } +// Jwk's kid was simplified to use just the tenants_jwks row's bare uuid +// Historically we embedded the kind resulting in a kid in the format "_" +// So a header's kid must be normalized to its id suffix before comparing against a jwk's (bare) kid +// this is to support existing JWTs that were already signed using the legacy format +function kidsMatch(keyKid: string | undefined, headerKid: string | undefined): boolean { + if (keyKid === undefined || headerKid === undefined) { + return false + } + // Bare-to-bare is the common case (and the only one post-rollout), so check it directly first. + return ( + keyKid === headerKid || normalizeUrlSigningKid(keyKid) === normalizeUrlSigningKid(headerKid) + ) +} + async function findJWKFromHeader( header: JWTHeaderParameters, secret: string, @@ -263,7 +278,7 @@ async function findJWKFromHeader( // find the first compatible "oct" key without a kid or with the matching kid let mismatchedJwk: JwksConfigKey | undefined const jwk = jwks.keys.find((key) => { - if ((!key.kid || key.kid === header.kid) && key.kty === 'oct' && key.k) { + if ((!key.kid || kidsMatch(key.kid, header.kid)) && key.kty === 'oct' && key.k) { if (key.alg !== undefined && key.alg !== header.alg) { mismatchedJwk ??= key return false diff --git a/src/test/tenant-jwks.test.ts b/src/test/tenant-jwks.test.ts index 69ec13acb..a94854cb6 100644 --- a/src/test/tenant-jwks.test.ts +++ b/src/test/tenant-jwks.test.ts @@ -14,7 +14,7 @@ mergeConfig({ import { encrypt, signJWT } from '@internal/auth' import { deleteTenantJwksConfig, JWKSManagerStorePg } from '@internal/auth/jwks' -import { TENANTS_JWKS_UPDATE_CHANNEL } from '@internal/auth/jwks/channels' +import { TENANTS_JWKS_UPDATE_CHANNEL } from '@internal/auth/jwks/constants' import { UrlSigningJwkGenerator } from '@internal/auth/jwks/generator' import { TENANT_JWKS_CACHE_NAME } from '@internal/cache' import {