Skip to content

Commit ffe13d7

Browse files
authored
feat: make getClaims() non experimental, add global cache (#1078)
`supabase.auth.getClaims()` loses `@expermental` status. To further improve performance since Vercel have now added Fluid Compute which shares a lot more memory between requests, every client's JWKS cache is stored in a global variable under the client's storage key.
1 parent e658156 commit ffe13d7

File tree

2 files changed

+74
-13
lines changed

2 files changed

+74
-13
lines changed

src/GoTrueClient.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promi
134134
return await fn()
135135
}
136136

137+
/**
138+
* Caches JWKS values for all clients created in the same environment. This is
139+
* especially useful for shared-memory execution environments such as Vercel's
140+
* Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
141+
* many clients are created, if they share the same storage key they will use
142+
* the same JWKS cache, significantly speeding up getClaims() with asymmetric
143+
* JWTs.
144+
*/
145+
const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}
146+
137147
export default class GoTrueClient {
138148
private static nextInstanceID = 0
139149

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

156166
protected flowType: AuthFlowType
167+
157168
/**
158169
* The JWKS used for verifying asymmetric JWTs
159170
*/
160-
protected jwks: { keys: JWK[] }
161-
protected jwks_cached_at: number
171+
protected get jwks() {
172+
return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
173+
}
174+
175+
protected set jwks(value: { keys: JWK[] }) {
176+
GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
177+
}
178+
179+
protected get jwks_cached_at() {
180+
return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
181+
}
182+
183+
protected set jwks_cached_at(value: number) {
184+
GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
185+
}
186+
162187
protected autoRefreshToken: boolean
163188
protected persistSession: boolean
164189
protected storage: SupportedStorage
@@ -242,8 +267,12 @@ export default class GoTrueClient {
242267
} else {
243268
this.lock = lockNoOp
244269
}
245-
this.jwks = { keys: [] }
246-
this.jwks_cached_at = Number.MIN_SAFE_INTEGER
270+
271+
if (!this.jwks) {
272+
this.jwks = { keys: [] }
273+
this.jwks_cached_at = Number.MIN_SAFE_INTEGER
274+
}
275+
247276
this.mfa = {
248277
verify: this._verify.bind(this),
249278
enroll: this._enroll.bind(this),
@@ -2946,11 +2975,13 @@ export default class GoTrueClient {
29462975
return jwk
29472976
}
29482977

2978+
const now = Date.now()
2979+
29492980
// try fetching from cache
29502981
jwk = this.jwks.keys.find((key) => key.kid === kid)
29512982

29522983
// jwk exists and jwks isn't stale
2953-
if (jwk && this.jwks_cached_at + JWKS_TTL > Date.now()) {
2984+
if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
29542985
return jwk
29552986
}
29562987
// jwk isn't cached in memory so we need to fetch it from the well-known endpoint
@@ -2963,8 +2994,10 @@ export default class GoTrueClient {
29632994
if (!data.keys || data.keys.length === 0) {
29642995
throw new AuthInvalidJwtError('JWKS is empty')
29652996
}
2997+
29662998
this.jwks = data
2967-
this.jwks_cached_at = Date.now()
2999+
this.jwks_cached_at = now
3000+
29683001
// Find the signing key
29693002
jwk = data.keys.find((key: any) => key.kid === kid)
29703003
if (!jwk) {
@@ -2974,12 +3007,35 @@ export default class GoTrueClient {
29743007
}
29753008

29763009
/**
2977-
* @experimental This method may change in future versions.
2978-
* @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.
3010+
* Extracts the JWT claims present in the access token by first verifying the
3011+
* JWT against the server's JSON Web Key Set endpoint
3012+
* `/.well-known/jwks.json` which is often cached, resulting in significantly
3013+
* faster responses. Prefer this method over {@link #getUser} which always
3014+
* sends a request to the Auth server for each JWT.
3015+
*
3016+
* If the project is not using an asymmetric JWT signing key (like ECC or
3017+
* RSA) it always sends a request to the Auth server (similar to {@link
3018+
* #getUser}) to verify the JWT.
3019+
*
3020+
* @param jwt An optional specific JWT you wish to verify, not the one you
3021+
* can obtain from {@link #getSession}.
3022+
* @param options Various additional options that allow you to customize the
3023+
* behavior of this method.
29793024
*/
29803025
async getClaims(
29813026
jwt?: string,
2982-
jwks: { keys: JWK[] } = { keys: [] }
3027+
options: {
3028+
/**
3029+
* @deprecated Please use options.jwks instead.
3030+
*/
3031+
keys?: JWK[]
3032+
3033+
/** If set to `true` the `exp` claim will not be validated against the current time. */
3034+
allowExpired?: boolean
3035+
3036+
/** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3037+
jwks?: { keys: JWK[] }
3038+
} = {}
29833039
): Promise<
29843040
| {
29853041
data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
@@ -3005,8 +3061,10 @@ export default class GoTrueClient {
30053061
raw: { header: rawHeader, payload: rawPayload },
30063062
} = decodeJWT(token)
30073063

3008-
// Reject expired JWTs
3009-
validateExp(payload.exp)
3064+
if (!options?.allowExpired) {
3065+
// Reject expired JWTs should only happen if jwt argument was passed
3066+
validateExp(payload.exp)
3067+
}
30103068

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

30323090
const algorithm = getAlgorithm(header.alg)
3033-
const signingKey = await this.fetchJwk(header.kid, jwks)
3091+
const signingKey = await this.fetchJwk(
3092+
header.kid,
3093+
options?.keys ? { keys: options.keys } : options?.jwks
3094+
)
30343095

30353096
// Convert JWK to CryptoKey
30363097
const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [

src/lib/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ export const API_VERSIONS = {
3131

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

34-
export const JWKS_TTL = 600000 // 10 minutes
34+
export const JWKS_TTL = 10 * 60 * 1000 // 10 minutes

0 commit comments

Comments
 (0)