@supabase/auth-js 2.110.9 → 2.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/main/GoTrueClient.d.ts +35 -2
  2. package/dist/main/GoTrueClient.d.ts.map +1 -1
  3. package/dist/main/GoTrueClient.js +129 -47
  4. package/dist/main/GoTrueClient.js.map +1 -1
  5. package/dist/main/lib/constants.d.ts +13 -0
  6. package/dist/main/lib/constants.d.ts.map +1 -1
  7. package/dist/main/lib/constants.js +14 -1
  8. package/dist/main/lib/constants.js.map +1 -1
  9. package/dist/main/lib/helpers.d.ts +50 -1
  10. package/dist/main/lib/helpers.d.ts.map +1 -1
  11. package/dist/main/lib/helpers.js +160 -4
  12. package/dist/main/lib/helpers.js.map +1 -1
  13. package/dist/main/lib/types.d.ts +38 -0
  14. package/dist/main/lib/types.d.ts.map +1 -1
  15. package/dist/main/lib/types.js.map +1 -1
  16. package/dist/main/lib/version.d.ts +1 -1
  17. package/dist/main/lib/version.js +1 -1
  18. package/dist/module/GoTrueClient.d.ts +35 -2
  19. package/dist/module/GoTrueClient.d.ts.map +1 -1
  20. package/dist/module/GoTrueClient.js +131 -49
  21. package/dist/module/GoTrueClient.js.map +1 -1
  22. package/dist/module/lib/constants.d.ts +13 -0
  23. package/dist/module/lib/constants.d.ts.map +1 -1
  24. package/dist/module/lib/constants.js +13 -0
  25. package/dist/module/lib/constants.js.map +1 -1
  26. package/dist/module/lib/helpers.d.ts +50 -1
  27. package/dist/module/lib/helpers.d.ts.map +1 -1
  28. package/dist/module/lib/helpers.js +152 -4
  29. package/dist/module/lib/helpers.js.map +1 -1
  30. package/dist/module/lib/types.d.ts +38 -0
  31. package/dist/module/lib/types.d.ts.map +1 -1
  32. package/dist/module/lib/types.js.map +1 -1
  33. package/dist/module/lib/version.d.ts +1 -1
  34. package/dist/module/lib/version.js +1 -1
  35. package/dist/tsconfig.module.tsbuildinfo +1 -1
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +1 -1
  38. package/src/GoTrueClient.ts +169 -67
  39. package/src/lib/constants.ts +15 -0
  40. package/src/lib/helpers.ts +197 -5
  41. package/src/lib/types.ts +38 -0
  42. package/src/lib/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/auth-js",
3
- "version": "2.110.9",
3
+ "version": "2.111.0",
4
4
  "private": false,
5
5
  "description": "Official SDK for Supabase Auth",
6
6
  "keywords": [
@@ -6,6 +6,7 @@ import {
6
6
  EXPIRY_MARGIN_MS,
7
7
  GOTRUE_URL,
8
8
  JWKS_TTL,
9
+ PKCE_FLOW_ID_PARAM,
9
10
  REFRESH_FAILURE_COOLDOWN_MS,
10
11
  STORAGE_KEY,
11
12
  } from './lib/constants'
@@ -36,6 +37,7 @@ import {
36
37
  _userResponse,
37
38
  } from './lib/fetch'
38
39
  import {
40
+ appendFlowIdToRedirectTo,
39
41
  assertPasskeyExperimentalEnabled,
40
42
  decodeJWT,
41
43
  deepClone,
@@ -47,14 +49,19 @@ import {
47
49
  insecureUserWarningProxy,
48
50
  isBrowser,
49
51
  parseParametersFromURL,
52
+ pkceVerifierSlotKey,
53
+ removeAllPKCEVerifiers,
50
54
  removeItemAsync,
55
+ removePKCEVerifier,
51
56
  resolveFetch,
57
+ retrievePKCEVerifier,
52
58
  retryable,
53
59
  setItemAsync,
54
60
  sleep,
55
61
  supportsLocalStorage,
56
62
  userNotAvailableProxy,
57
63
  validateExp,
64
+ validatePKCEFlowId,
58
65
  } from './lib/helpers'
59
66
  import { memoryLocalStorageAdapter } from './lib/local-storage'
60
67
  import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
@@ -1005,6 +1012,7 @@ export default class GoTrueClient {
1005
1012
  * ```
1006
1013
  */
1007
1014
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
1015
+ let flowId: string | null = null
1008
1016
  try {
1009
1017
  let res: AuthResponse
1010
1018
  if ('email' in credentials) {
@@ -1012,14 +1020,11 @@ export default class GoTrueClient {
1012
1020
  let codeChallenge: string | null = null
1013
1021
  let codeChallengeMethod: string | null = null
1014
1022
  if (this.flowType === 'pkce') {
1015
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1016
- this.storage,
1017
- this.storageKey
1018
- )
1023
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
1019
1024
  }
1020
1025
  res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
1021
1026
  headers: this.headers,
1022
- redirectTo: options?.emailRedirectTo,
1027
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
1023
1028
  body: {
1024
1029
  email,
1025
1030
  password,
@@ -1052,7 +1057,7 @@ export default class GoTrueClient {
1052
1057
  const { data, error } = res
1053
1058
 
1054
1059
  if (error || !data) {
1055
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1060
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1056
1061
  return this._returnResult({ data: { user: null, session: null }, error: error })
1057
1062
  }
1058
1063
 
@@ -1066,7 +1071,7 @@ export default class GoTrueClient {
1066
1071
 
1067
1072
  return this._returnResult({ data: { user, session }, error: null })
1068
1073
  } catch (error) {
1069
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1074
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1070
1075
  if (isAuthError(error)) {
1071
1076
  return this._returnResult({ data: { user: null, session: null }, error })
1072
1077
  }
@@ -1289,7 +1294,8 @@ export default class GoTrueClient {
1289
1294
  * {
1290
1295
  * data: {
1291
1296
  * provider: 'github',
1292
- * url: <PROVIDER_URL_TO_REDIRECT_TO>
1297
+ * url: <PROVIDER_URL_TO_REDIRECT_TO>,
1298
+ * flowId: <PKCE_FLOW_ID_OR_NULL>
1293
1299
  * },
1294
1300
  * error: null
1295
1301
  * }
@@ -1362,12 +1368,29 @@ export default class GoTrueClient {
1362
1368
  *
1363
1369
  * @remarks
1364
1370
  * - Used when `flowType` is set to `pkce` in client options.
1371
+ * - When several PKCE flows are in flight at once, pass `options.flowId` so
1372
+ * the code is exchanged with the verifier created by that specific flow.
1373
+ * The flow id is returned by `signInWithOAuth`, and with
1374
+ * `experimental.appendPkceFlowIdToRedirects` enabled it also arrives on
1375
+ * your callback URL as the reserved `sb_flow_id` query parameter (read
1376
+ * automatically in a browser).
1377
+ * - When a flow id is present but its stored verifier is gone (evicted,
1378
+ * already used, or from another device), the call fails with a verifier
1379
+ * missing error instead of trying another flow's verifier — a mismatched
1380
+ * verifier would consume the single-use code. Without any flow id the
1381
+ * most recently stored verifier is used, as before.
1365
1382
  *
1366
1383
  * @example Exchange Auth Code
1367
1384
  * ```js
1368
1385
  * supabase.auth.exchangeCodeForSession('34e770dd-9ff9-416c-87fa-43b31d7ef225')
1369
1386
  * ```
1370
1387
  *
1388
+ * @example Exchange Auth Code for a specific flow (e.g. in a server-side callback handler)
1389
+ * ```js
1390
+ * const flowId = requestUrl.searchParams.get('sb_flow_id')
1391
+ * supabase.auth.exchangeCodeForSession(code, flowId ? { flowId } : undefined)
1392
+ * ```
1393
+ *
1371
1394
  * @exampleResponse Exchange Auth Code
1372
1395
  * ```json
1373
1396
  * {
@@ -1525,17 +1548,20 @@ export default class GoTrueClient {
1525
1548
  * }
1526
1549
  * ```
1527
1550
  */
1528
- async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
1551
+ async exchangeCodeForSession(
1552
+ authCode: string,
1553
+ options?: { flowId?: string }
1554
+ ): Promise<AuthTokenResponse> {
1529
1555
  await this.initializePromise
1530
1556
 
1531
1557
  if (this.lock != null) {
1532
1558
  // TODO(v3): remove legacy lock path
1533
1559
  return this._acquireLock(this.lockAcquireTimeout, async () => {
1534
- return this._exchangeCodeForSession(authCode)
1560
+ return this._exchangeCodeForSession(authCode, options)
1535
1561
  })
1536
1562
  }
1537
1563
 
1538
- return this._exchangeCodeForSession(authCode)
1564
+ return this._exchangeCodeForSession(authCode, options)
1539
1565
  }
1540
1566
 
1541
1567
  /**
@@ -1968,15 +1994,40 @@ export default class GoTrueClient {
1968
1994
  }
1969
1995
  }
1970
1996
 
1971
- private async _exchangeCodeForSession(authCode: string): Promise<
1997
+ private async _exchangeCodeForSession(
1998
+ authCode: string,
1999
+ options?: { flowId?: string }
2000
+ ): Promise<
1972
2001
  | {
1973
2002
  data: { session: Session; user: User; redirectType: string | null }
1974
2003
  error: null
1975
2004
  }
1976
2005
  | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1977
2006
  > {
1978
- const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1979
- const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
2007
+ const hasExplicitFlowId = options?.flowId != null
2008
+ const requestedFlowId = hasExplicitFlowId
2009
+ ? validatePKCEFlowId(options?.flowId)
2010
+ : isBrowser()
2011
+ ? validatePKCEFlowId(parseParametersFromURL(window.location.href)[PKCE_FLOW_ID_PARAM])
2012
+ : null
2013
+
2014
+ if (hasExplicitFlowId && !requestedFlowId) {
2015
+ this._debug(
2016
+ '#_exchangeCodeForSession()',
2017
+ 'provided flowId is not a valid flow id',
2018
+ options?.flowId
2019
+ )
2020
+ }
2021
+
2022
+ // With a flow id (explicit or from the callback URL) the lookup is
2023
+ // slot-only and a miss fails fast — see retrievePKCEVerifier. An invalid
2024
+ // explicit flow id also fails fast rather than borrowing another flow's
2025
+ // verifier.
2026
+ const { verifier: storageItem, flowId } =
2027
+ hasExplicitFlowId && !requestedFlowId
2028
+ ? { verifier: null, flowId: null }
2029
+ : await retrievePKCEVerifier(this.storage, this.storageKey, requestedFlowId)
2030
+ const [codeVerifier, redirectType] = (storageItem ?? '').split('/')
1980
2031
 
1981
2032
  try {
1982
2033
  if (!codeVerifier && this.flowType === 'pkce') {
@@ -1996,7 +2047,7 @@ export default class GoTrueClient {
1996
2047
  xform: _sessionResponse,
1997
2048
  }
1998
2049
  )
1999
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2050
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2000
2051
  if (error) {
2001
2052
  throw error
2002
2053
  }
@@ -2016,7 +2067,7 @@ export default class GoTrueClient {
2016
2067
  }
2017
2068
  return this._returnResult({ data: { ...data, redirectType: redirectType ?? null }, error })
2018
2069
  } catch (error) {
2019
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2070
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2020
2071
  if (isAuthError(error)) {
2021
2072
  return this._returnResult({
2022
2073
  data: { user: null, session: null, redirectType: null },
@@ -2216,16 +2267,14 @@ export default class GoTrueClient {
2216
2267
  * ```
2217
2268
  */
2218
2269
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
2270
+ let flowId: string | null = null
2219
2271
  try {
2220
2272
  if ('email' in credentials) {
2221
2273
  const { email, options } = credentials
2222
2274
  let codeChallenge: string | null = null
2223
2275
  let codeChallengeMethod: string | null = null
2224
2276
  if (this.flowType === 'pkce') {
2225
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2226
- this.storage,
2227
- this.storageKey
2228
- )
2277
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2229
2278
  }
2230
2279
  const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2231
2280
  headers: this.headers,
@@ -2237,7 +2286,7 @@ export default class GoTrueClient {
2237
2286
  code_challenge: codeChallenge,
2238
2287
  code_challenge_method: codeChallengeMethod,
2239
2288
  },
2240
- redirectTo: options?.emailRedirectTo,
2289
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
2241
2290
  })
2242
2291
  return this._returnResult({ data: { user: null, session: null }, error })
2243
2292
  }
@@ -2260,7 +2309,7 @@ export default class GoTrueClient {
2260
2309
  }
2261
2310
  throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
2262
2311
  } catch (error) {
2263
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2312
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2264
2313
  if (isAuthError(error)) {
2265
2314
  return this._returnResult({ data: { user: null, session: null }, error })
2266
2315
  }
@@ -2507,21 +2556,19 @@ export default class GoTrueClient {
2507
2556
  * ```
2508
2557
  */
2509
2558
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
2559
+ let flowId: string | null = null
2510
2560
  try {
2511
2561
  let codeChallenge: string | null = null
2512
2562
  let codeChallengeMethod: string | null = null
2513
2563
  if (this.flowType === 'pkce') {
2514
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2515
- this.storage,
2516
- this.storageKey
2517
- )
2564
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2518
2565
  }
2519
2566
 
2520
2567
  const result = await _request(this.fetch, 'POST', `${this.url}/sso`, {
2521
2568
  body: {
2522
2569
  ...('providerId' in params ? { provider_id: params.providerId } : null),
2523
2570
  ...('domain' in params ? { domain: params.domain } : null),
2524
- redirect_to: params.options?.redirectTo ?? undefined,
2571
+ redirect_to: this._maybeAppendFlowIdToRedirect(params.options?.redirectTo, flowId),
2525
2572
  ...(params?.options?.captchaToken
2526
2573
  ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
2527
2574
  : null),
@@ -2540,7 +2587,7 @@ export default class GoTrueClient {
2540
2587
 
2541
2588
  return this._returnResult(result)
2542
2589
  } catch (error) {
2543
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2590
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2544
2591
  if (isAuthError(error)) {
2545
2592
  return this._returnResult({ data: null, error })
2546
2593
  }
@@ -2666,6 +2713,7 @@ export default class GoTrueClient {
2666
2713
  * ```
2667
2714
  */
2668
2715
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
2716
+ let flowId: string | null = null
2669
2717
  try {
2670
2718
  const endpoint = `${this.url}/resend`
2671
2719
  if ('email' in credentials) {
@@ -2673,10 +2721,7 @@ export default class GoTrueClient {
2673
2721
  let codeChallenge: string | null = null
2674
2722
  let codeChallengeMethod: string | null = null
2675
2723
  if (this.flowType === 'pkce') {
2676
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2677
- this.storage,
2678
- this.storageKey
2679
- )
2724
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2680
2725
  }
2681
2726
  const { error } = await _request(this.fetch, 'POST', endpoint, {
2682
2727
  headers: this.headers,
@@ -2687,10 +2732,10 @@ export default class GoTrueClient {
2687
2732
  code_challenge: codeChallenge,
2688
2733
  code_challenge_method: codeChallengeMethod,
2689
2734
  },
2690
- redirectTo: options?.emailRedirectTo,
2735
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
2691
2736
  })
2692
2737
  if (error) {
2693
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2738
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2694
2739
  }
2695
2740
  return this._returnResult({ data: { user: null, session: null }, error })
2696
2741
  } else if ('phone' in credentials) {
@@ -2712,7 +2757,7 @@ export default class GoTrueClient {
2712
2757
  'You must provide either an email or phone number and a type'
2713
2758
  )
2714
2759
  } catch (error) {
2715
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2760
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2716
2761
  if (isAuthError(error)) {
2717
2762
  return this._returnResult({ data: { user: null, session: null }, error })
2718
2763
  }
@@ -3207,7 +3252,6 @@ export default class GoTrueClient {
3207
3252
  // session in the database, indicating the user is signed out.
3208
3253
 
3209
3254
  await this._removeSession()
3210
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
3211
3255
  }
3212
3256
 
3213
3257
  return this._returnResult({ data: { user: null }, error })
@@ -3355,6 +3399,7 @@ export default class GoTrueClient {
3355
3399
  emailRedirectTo?: string | undefined
3356
3400
  } = {}
3357
3401
  ): Promise<UserResponse> {
3402
+ let flowId: string | null = null
3358
3403
  try {
3359
3404
  return await this._useSession(async (result) => {
3360
3405
  const { data: sessionData, error: sessionError } = result
@@ -3368,15 +3413,12 @@ export default class GoTrueClient {
3368
3413
  let codeChallenge: string | null = null
3369
3414
  let codeChallengeMethod: string | null = null
3370
3415
  if (this.flowType === 'pkce' && attributes.email != null) {
3371
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
3372
- this.storage,
3373
- this.storageKey
3374
- )
3416
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
3375
3417
  }
3376
3418
 
3377
3419
  const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
3378
3420
  headers: this.headers,
3379
- redirectTo: options?.emailRedirectTo,
3421
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
3380
3422
  body: {
3381
3423
  ...attributes,
3382
3424
  code_challenge: codeChallenge,
@@ -3394,7 +3436,7 @@ export default class GoTrueClient {
3394
3436
  return this._returnResult({ data: { user: session.user }, error: null })
3395
3437
  })
3396
3438
  } catch (error) {
3397
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
3439
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
3398
3440
  if (isAuthError(error)) {
3399
3441
  return this._returnResult({ data: { user: null }, error })
3400
3442
  }
@@ -3826,11 +3868,14 @@ export default class GoTrueClient {
3826
3868
  if (callbackUrlType === 'pkce') {
3827
3869
  this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
3828
3870
  if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
3829
- const { data, error } = await this._exchangeCodeForSession(params.code)
3871
+ const { data, error } = await this._exchangeCodeForSession(params.code, {
3872
+ flowId: params[PKCE_FLOW_ID_PARAM],
3873
+ })
3830
3874
  if (error) throw error
3831
3875
 
3832
3876
  const url = new URL(window.location.href)
3833
3877
  url.searchParams.delete('code')
3878
+ url.searchParams.delete(PKCE_FLOW_ID_PARAM)
3834
3879
 
3835
3880
  window.history.replaceState(window.history.state, '', url.toString())
3836
3881
 
@@ -3934,12 +3979,24 @@ export default class GoTrueClient {
3934
3979
  * Checks if the current URL and backing storage contain parameters given by a PKCE flow
3935
3980
  */
3936
3981
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
3982
+ if (!params.code) {
3983
+ return false
3984
+ }
3985
+
3986
+ const flowId = validatePKCEFlowId(params[PKCE_FLOW_ID_PARAM])
3987
+ if (
3988
+ flowId &&
3989
+ (await getItemAsync(this.storage, pkceVerifierSlotKey(this.storageKey, flowId)))
3990
+ ) {
3991
+ return true
3992
+ }
3993
+
3937
3994
  const currentStorageContent = await getItemAsync(
3938
3995
  this.storage,
3939
3996
  `${this.storageKey}-code-verifier`
3940
3997
  )
3941
3998
 
3942
- return !!(params.code && currentStorageContent)
3999
+ return !!currentStorageContent
3943
4000
  }
3944
4001
 
3945
4002
  /**
@@ -4002,7 +4059,6 @@ export default class GoTrueClient {
4002
4059
  return await this._useSession(async (result) => {
4003
4060
  const removeCurrentSession = async () => {
4004
4061
  await this._removeSession()
4005
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4006
4062
  }
4007
4063
  const { data, error: sessionError } = result
4008
4064
  if (sessionError && !isAuthSessionMissingError(sessionError)) {
@@ -4392,11 +4448,10 @@ export default class GoTrueClient {
4392
4448
  > {
4393
4449
  let codeChallenge: string | null = null
4394
4450
  let codeChallengeMethod: string | null = null
4451
+ let flowId: string | null = null
4395
4452
 
4396
4453
  if (this.flowType === 'pkce') {
4397
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4398
- this.storage,
4399
- this.storageKey,
4454
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod(
4400
4455
  true // isPasswordRecovery
4401
4456
  )
4402
4457
  }
@@ -4409,10 +4464,10 @@ export default class GoTrueClient {
4409
4464
  gotrue_meta_security: { captcha_token: options.captchaToken },
4410
4465
  },
4411
4466
  headers: this.headers,
4412
- redirectTo: options.redirectTo,
4467
+ redirectTo: this._maybeAppendFlowIdToRedirect(options.redirectTo, flowId),
4413
4468
  })
4414
4469
  } catch (error) {
4415
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4470
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
4416
4471
  if (isAuthError(error)) {
4417
4472
  return this._returnResult({ data: null, error })
4418
4473
  }
@@ -4514,7 +4569,8 @@ export default class GoTrueClient {
4514
4569
  * {
4515
4570
  * data: {
4516
4571
  * provider: 'github',
4517
- * url: <PROVIDER_URL_TO_REDIRECT_TO>
4572
+ * url: <PROVIDER_URL_TO_REDIRECT_TO>,
4573
+ * flowId: <PKCE_FLOW_ID_OR_NULL>
4518
4574
  * },
4519
4575
  * error: null
4520
4576
  * }
@@ -4529,11 +4585,12 @@ export default class GoTrueClient {
4529
4585
  }
4530
4586
 
4531
4587
  private async linkIdentityOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
4588
+ let flowId: string | null = null
4532
4589
  try {
4533
4590
  const { data, error } = await this._useSession(async (result) => {
4534
4591
  const { data, error } = result
4535
4592
  if (error) throw error
4536
- const url: string = await this._getUrlForProvider(
4593
+ const { url, flowId: urlFlowId } = await this._getUrlForProvider(
4537
4594
  `${this.url}/user/identities/authorize`,
4538
4595
  credentials.provider,
4539
4596
  {
@@ -4543,6 +4600,7 @@ export default class GoTrueClient {
4543
4600
  skipBrowserRedirect: true,
4544
4601
  }
4545
4602
  )
4603
+ flowId = urlFlowId
4546
4604
  return await _request(this.fetch, 'GET', url, {
4547
4605
  headers: this.headers,
4548
4606
  jwt: data.session?.access_token ?? undefined,
@@ -4553,12 +4611,15 @@ export default class GoTrueClient {
4553
4611
  window.location.assign(data?.url)
4554
4612
  }
4555
4613
  return this._returnResult({
4556
- data: { provider: credentials.provider, url: data?.url },
4614
+ data: { provider: credentials.provider, url: data?.url, flowId },
4557
4615
  error: null,
4558
4616
  })
4559
4617
  } catch (error) {
4560
4618
  if (isAuthError(error)) {
4561
- return this._returnResult({ data: { provider: credentials.provider, url: null }, error })
4619
+ return this._returnResult({
4620
+ data: { provider: credentials.provider, url: null, flowId },
4621
+ error,
4622
+ })
4562
4623
  }
4563
4624
  throw error
4564
4625
  }
@@ -4606,7 +4667,7 @@ export default class GoTrueClient {
4606
4667
  }
4607
4668
  return this._returnResult({ data, error })
4608
4669
  } catch (error) {
4609
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4670
+ await removePKCEVerifier(this.storage, this.storageKey, null)
4610
4671
  if (isAuthError(error)) {
4611
4672
  return this._returnResult({ data: { user: null, session: null }, error })
4612
4673
  }
@@ -4742,7 +4803,7 @@ export default class GoTrueClient {
4742
4803
  skipBrowserRedirect?: boolean
4743
4804
  }
4744
4805
  ) {
4745
- const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
4806
+ const { url, flowId } = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
4746
4807
  redirectTo: options.redirectTo,
4747
4808
  scopes: options.scopes,
4748
4809
  queryParams: options.queryParams,
@@ -4755,7 +4816,7 @@ export default class GoTrueClient {
4755
4816
  window.location.assign(url)
4756
4817
  }
4757
4818
 
4758
- return { data: { provider, url }, error: null }
4819
+ return { data: { provider, url, flowId }, error: null }
4759
4820
  }
4760
4821
 
4761
4822
  /**
@@ -5176,7 +5237,7 @@ export default class GoTrueClient {
5176
5237
  this.suppressGetSessionWarning = false
5177
5238
 
5178
5239
  await removeItemAsync(this.storage, this.storageKey)
5179
- await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
5240
+ await removeAllPKCEVerifiers(this.storage, this.storageKey)
5180
5241
  await removeItemAsync(this.storage, this.storageKey + '-user')
5181
5242
 
5182
5243
  if (this.userStorage) {
@@ -5600,19 +5661,23 @@ export default class GoTrueClient {
5600
5661
  skipBrowserRedirect?: boolean
5601
5662
  }
5602
5663
  ) {
5664
+ let redirectTo = options?.redirectTo
5665
+ let codeChallenge: string | null = null
5666
+ let codeChallengeMethod: string | null = null
5667
+ let flowId: string | null = null
5668
+ if (this.flowType === 'pkce') {
5669
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
5670
+ redirectTo = this._maybeAppendFlowIdToRedirect(redirectTo, flowId)
5671
+ }
5672
+
5603
5673
  const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
5604
- if (options?.redirectTo) {
5605
- urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
5674
+ if (redirectTo) {
5675
+ urlParams.push(`redirect_to=${encodeURIComponent(redirectTo)}`)
5606
5676
  }
5607
5677
  if (options?.scopes) {
5608
5678
  urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
5609
5679
  }
5610
- if (this.flowType === 'pkce') {
5611
- const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
5612
- this.storage,
5613
- this.storageKey
5614
- )
5615
-
5680
+ if (codeChallenge != null && codeChallengeMethod != null) {
5616
5681
  const flowParams = new URLSearchParams({
5617
5682
  code_challenge: `${encodeURIComponent(codeChallenge)}`,
5618
5683
  code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
@@ -5627,7 +5692,44 @@ export default class GoTrueClient {
5627
5692
  urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
5628
5693
  }
5629
5694
 
5630
- return `${url}?${urlParams.join('&')}`
5695
+ return { url: `${url}?${urlParams.join('&')}`, flowId }
5696
+ }
5697
+
5698
+ /**
5699
+ * Appends the reserved flow id parameter to a redirect URL so the callback
5700
+ * can be matched to the verifier stored for its flow. Opt-in via
5701
+ * `experimental.appendPkceFlowIdToRedirects`: redirect URLs are validated
5702
+ * against the project's allow list including the query string, so an extra
5703
+ * parameter can stop exact (non-wildcard) entries from matching.
5704
+ */
5705
+ private _maybeAppendFlowIdToRedirect(
5706
+ redirectTo: string | undefined,
5707
+ flowId: string | null
5708
+ ): string | undefined {
5709
+ if (!redirectTo || !flowId || !this.experimental.appendPkceFlowIdToRedirects) {
5710
+ return redirectTo ?? undefined
5711
+ }
5712
+ return appendFlowIdToRedirectTo(redirectTo, flowId)
5713
+ }
5714
+
5715
+ /**
5716
+ * Generates and stores a PKCE challenge/verifier pair for a new flow,
5717
+ * logging any pending verifier the bounded slot ring evicts.
5718
+ */
5719
+ private async _getCodeChallengeAndMethod(
5720
+ isPasswordRecovery = false
5721
+ ): Promise<[string, string, string]> {
5722
+ return getCodeChallengeAndMethod(
5723
+ this.storage,
5724
+ this.storageKey,
5725
+ isPasswordRecovery,
5726
+ (evictedFlowId) =>
5727
+ this._debug(
5728
+ '#_getCodeChallengeAndMethod()',
5729
+ 'evicted oldest pending PKCE verifier slot',
5730
+ evictedFlowId
5731
+ )
5732
+ )
5631
5733
  }
5632
5734
 
5633
5735
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
@@ -40,4 +40,19 @@ export const API_VERSIONS = {
40
40
 
41
41
  export const BASE64URL_REGEX = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i
42
42
 
43
+ /**
44
+ * Reserved query parameter appended to `redirectTo` URLs (behind
45
+ * `experimental.appendPkceFlowIdToRedirects`) that correlates a PKCE callback
46
+ * with the code verifier stored when its flow started. It round-trips through
47
+ * the auth server untouched and identifies a verifier slot in storage — the
48
+ * verifier itself never appears in a URL.
49
+ */
50
+ export const PKCE_FLOW_ID_PARAM = 'sb_flow_id'
51
+
52
+ /**
53
+ * Maximum number of PKCE code verifiers kept in storage at once. Starting a
54
+ * new flow beyond this evicts the oldest pending verifier.
55
+ */
56
+ export const PKCE_MAX_CONCURRENT_FLOWS = 5
57
+
43
58
  export const JWKS_TTL = 10 * 60 * 1000 // 10 minutes