From 1234c1b5a7ac902251019f3e26f5edff0f73de14 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Wed, 9 Jul 2025 13:51:55 +0200 Subject: [PATCH 1/2] feat: fallback to `getUser()` if the `kid` of the JWT is not found --- src/GoTrueClient.ts | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 08919c82..e463ef4f 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -2968,7 +2968,7 @@ export default class GoTrueClient { }) } - private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise { + private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise { // try fetching from the supplied jwks let jwk = jwks.keys.find((key) => key.kid === kid) if (jwk) { @@ -2992,7 +2992,7 @@ export default class GoTrueClient { throw error } if (!data.keys || data.keys.length === 0) { - throw new AuthInvalidJwtError('JWKS is empty') + return null } this.jwks = data @@ -3001,7 +3001,7 @@ export default class GoTrueClient { // Find the signing key jwk = data.keys.find((key: any) => key.kid === kid) if (!jwk) { - throw new AuthInvalidJwtError('No matching signing key found in JWKS') + return null } return jwk } @@ -3093,21 +3093,40 @@ export default class GoTrueClient { options?.keys ? { keys: options.keys } : options?.jwks ) - // Convert JWK to CryptoKey - const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [ - 'verify', - ]) + if (signingKey) { + // Convert JWK to CryptoKey + const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [ + 'verify', + ]) - // Verify the signature - const isValid = await crypto.subtle.verify( - algorithm, - publicKey, - signature, - stringToUint8Array(`${rawHeader}.${rawPayload}`) - ) + // Verify the signature + const isValid = await crypto.subtle.verify( + algorithm, + publicKey, + signature, + stringToUint8Array(`${rawHeader}.${rawPayload}`) + ) + + if (!isValid) { + throw new AuthInvalidJwtError('Invalid JWT signature') + } + } else { + // no signing key found in the JWKS, this might mean that the developer rotated the JWT signing key too fast without waiting for all caches to be purged + // in this case, validate the JWT directly with the Auth server - if (!isValid) { - throw new AuthInvalidJwtError('Invalid JWT signature') + const { error } = await this.getUser(token) + if (error) { + throw error + } + // getUser succeeds so the claims in the JWT can be trusted + return { + data: { + claims: payload, + header, + signature, + }, + error: null, + } } // If verification succeeds, decode and return claims From 361baf1b21cb516d149ad1f8646364a0943cd39d Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Wed, 9 Jul 2025 21:23:28 +0200 Subject: [PATCH 2/2] further simplify code --- src/GoTrueClient.ts | 61 ++++++++++++++++----------------------------- src/lib/helpers.ts | 4 ++- 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index e463ef4f..082a0899 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -3066,12 +3066,16 @@ export default class GoTrueClient { validateExp(payload.exp) } - // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser() - if ( + const signingKey = + !header.alg || + header.alg.startsWith('HS') || !header.kid || - header.alg === 'HS256' || !('crypto' in globalThis && 'subtle' in globalThis.crypto) - ) { + ? null + : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks) + + // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser() + if (!signingKey) { const { error } = await this.getUser(token) if (error) { throw error @@ -3088,45 +3092,22 @@ export default class GoTrueClient { } const algorithm = getAlgorithm(header.alg) - const signingKey = await this.fetchJwk( - header.kid, - options?.keys ? { keys: options.keys } : options?.jwks - ) - if (signingKey) { - // Convert JWK to CryptoKey - const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [ - 'verify', - ]) + // Convert JWK to CryptoKey + const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [ + 'verify', + ]) - // Verify the signature - const isValid = await crypto.subtle.verify( - algorithm, - publicKey, - signature, - stringToUint8Array(`${rawHeader}.${rawPayload}`) - ) - - if (!isValid) { - throw new AuthInvalidJwtError('Invalid JWT signature') - } - } else { - // no signing key found in the JWKS, this might mean that the developer rotated the JWT signing key too fast without waiting for all caches to be purged - // in this case, validate the JWT directly with the Auth server + // Verify the signature + const isValid = await crypto.subtle.verify( + algorithm, + publicKey, + signature, + stringToUint8Array(`${rawHeader}.${rawPayload}`) + ) - const { error } = await this.getUser(token) - if (error) { - throw error - } - // getUser succeeds so the claims in the JWT can be trusted - return { - data: { - claims: payload, - header, - signature, - }, - error: null, - } + if (!isValid) { + throw new AuthInvalidJwtError('Invalid JWT signature') } // If verification succeeds, decode and return claims diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 6dae3511..a87f0776 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -340,7 +340,9 @@ export function validateExp(exp: number) { } } -export function getAlgorithm(alg: 'RS256' | 'ES256'): RsaHashedImportParams | EcKeyImportParams { +export function getAlgorithm( + alg: 'HS256' | 'RS256' | 'ES256' +): RsaHashedImportParams | EcKeyImportParams { switch (alg) { case 'RS256': return {