Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/internal/auth/jwks/channels.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/internal/auth/jwks/constants.ts
Original file line number Diff line number Diff line change
@@ -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'
15 changes: 15 additions & 0 deletions src/internal/auth/jwks/kid.ts
Original file line number Diff line number Diff line change
@@ -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 "<kind>_" 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
}
6 changes: 2 additions & 4 deletions src/internal/auth/jwks/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JwksConfig>()
// 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
Expand Down
17 changes: 16 additions & 1 deletion src/internal/auth/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SignJWT,
} from 'jose'
import { getConfig, JwksConfig, JwksConfigKey, JwksConfigKeyOCT } from '../../config'
import { normalizeUrlSigningKid } from './jwks/kid'

const { jwtAlgorithm } = getConfig()

Expand Down Expand Up @@ -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 "<kind>_<id>"
// 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.
Comment thread
itslenny marked this conversation as resolved.
return (
keyKid === headerKid || normalizeUrlSigningKid(keyKid) === normalizeUrlSigningKid(headerKid)
)
}

async function findJWKFromHeader(
header: JWTHeaderParameters,
secret: string,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/test/tenant-jwks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down