Skip to content

feat: make getClaims() non experimental, add global cache #1078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 9, 2025
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
85 changes: 73 additions & 12 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promi
return await fn()
}

/**
* Caches JWKS values for all clients created in the same environment. This is
* especially useful for shared-memory execution environments such as Vercel's
* Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
* many clients are created, if they share the same storage key they will use
* the same JWKS cache, significantly speeding up getClaims() with asymmetric
* JWTs.
*/
const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}

export default class GoTrueClient {
private static nextInstanceID = 0

Expand All @@ -154,11 +164,26 @@ export default class GoTrueClient {
protected storageKey: string

protected flowType: AuthFlowType

/**
* The JWKS used for verifying asymmetric JWTs
*/
protected jwks: { keys: JWK[] }
protected jwks_cached_at: number
protected get jwks() {
return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
}

protected set jwks(value: { keys: JWK[] }) {
GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
}

protected get jwks_cached_at() {
return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
}

protected set jwks_cached_at(value: number) {
GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
}

protected autoRefreshToken: boolean
protected persistSession: boolean
protected storage: SupportedStorage
Expand Down Expand Up @@ -242,8 +267,12 @@ export default class GoTrueClient {
} else {
this.lock = lockNoOp
}
this.jwks = { keys: [] }
this.jwks_cached_at = Number.MIN_SAFE_INTEGER

if (!this.jwks) {
this.jwks = { keys: [] }
this.jwks_cached_at = Number.MIN_SAFE_INTEGER
}

this.mfa = {
verify: this._verify.bind(this),
enroll: this._enroll.bind(this),
Expand Down Expand Up @@ -2946,11 +2975,13 @@ export default class GoTrueClient {
return jwk
}

const now = Date.now()

// try fetching from cache
jwk = this.jwks.keys.find((key) => key.kid === kid)

// jwk exists and jwks isn't stale
if (jwk && this.jwks_cached_at + JWKS_TTL > Date.now()) {
if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
return jwk
}
// jwk isn't cached in memory so we need to fetch it from the well-known endpoint
Expand All @@ -2963,8 +2994,10 @@ export default class GoTrueClient {
if (!data.keys || data.keys.length === 0) {
throw new AuthInvalidJwtError('JWKS is empty')
}

this.jwks = data
this.jwks_cached_at = Date.now()
this.jwks_cached_at = now

// Find the signing key
jwk = data.keys.find((key: any) => key.kid === kid)
if (!jwk) {
Expand All @@ -2974,12 +3007,35 @@ export default class GoTrueClient {
}

/**
* @experimental This method may change in future versions.
* @description Gets the claims from a JWT. If the JWT is symmetric JWTs, it will call getUser() to verify against the server. If the JWT is asymmetric, it will be verified against the JWKS using the WebCrypto API.
* Extracts the JWT claims present in the access token by first verifying the
* JWT against the server's JSON Web Key Set endpoint
* `/.well-known/jwks.json` which is often cached, resulting in significantly
* faster responses. Prefer this method over {@link #getUser} which always
* sends a request to the Auth server for each JWT.
*
* If the project is not using an asymmetric JWT signing key (like ECC or
* RSA) it always sends a request to the Auth server (similar to {@link
* #getUser}) to verify the JWT.
*
* @param jwt An optional specific JWT you wish to verify, not the one you
* can obtain from {@link #getSession}.
* @param options Various additional options that allow you to customize the
* behavior of this method.
*/
async getClaims(
jwt?: string,
jwks: { keys: JWK[] } = { keys: [] }
options: {
/**
* @deprecated Please use options.jwks instead.
*/
keys?: JWK[]

/** If set to `true` the `exp` claim will not be validated against the current time. */
allowExpired?: boolean

/** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
jwks?: { keys: JWK[] }
} = {}
): Promise<
| {
data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
Expand All @@ -3005,8 +3061,10 @@ export default class GoTrueClient {
raw: { header: rawHeader, payload: rawPayload },
} = decodeJWT(token)

// Reject expired JWTs
validateExp(payload.exp)
if (!options?.allowExpired) {
// Reject expired JWTs should only happen if jwt argument was passed
validateExp(payload.exp)
}

// If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
if (
Expand All @@ -3030,7 +3088,10 @@ export default class GoTrueClient {
}

const algorithm = getAlgorithm(header.alg)
const signingKey = await this.fetchJwk(header.kid, jwks)
const signingKey = await this.fetchJwk(
header.kid,
options?.keys ? { keys: options.keys } : options?.jwks
)

// Convert JWK to CryptoKey
const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ export const API_VERSIONS = {

export const BASE64URL_REGEX = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i

export const JWKS_TTL = 600000 // 10 minutes
export const JWKS_TTL = 10 * 60 * 1000 // 10 minutes