@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.
- package/dist/main/GoTrueClient.d.ts +35 -2
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +129 -47
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/constants.d.ts +13 -0
- package/dist/main/lib/constants.d.ts.map +1 -1
- package/dist/main/lib/constants.js +14 -1
- package/dist/main/lib/constants.js.map +1 -1
- package/dist/main/lib/helpers.d.ts +50 -1
- package/dist/main/lib/helpers.d.ts.map +1 -1
- package/dist/main/lib/helpers.js +160 -4
- package/dist/main/lib/helpers.js.map +1 -1
- package/dist/main/lib/types.d.ts +38 -0
- package/dist/main/lib/types.d.ts.map +1 -1
- package/dist/main/lib/types.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/module/GoTrueClient.d.ts +35 -2
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +131 -49
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/constants.d.ts +13 -0
- package/dist/module/lib/constants.d.ts.map +1 -1
- package/dist/module/lib/constants.js +13 -0
- package/dist/module/lib/constants.js.map +1 -1
- package/dist/module/lib/helpers.d.ts +50 -1
- package/dist/module/lib/helpers.d.ts.map +1 -1
- package/dist/module/lib/helpers.js +152 -4
- package/dist/module/lib/helpers.js.map +1 -1
- package/dist/module/lib/types.d.ts +38 -0
- package/dist/module/lib/types.d.ts.map +1 -1
- package/dist/module/lib/types.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/GoTrueClient.ts +169 -67
- package/src/lib/constants.ts +15 -0
- package/src/lib/helpers.ts +197 -5
- package/src/lib/types.ts +38 -0
- package/src/lib/version.ts +1 -1
package/src/lib/helpers.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
API_VERSION_HEADER_NAME,
|
|
3
|
+
BASE64URL_REGEX,
|
|
4
|
+
PKCE_FLOW_ID_PARAM,
|
|
5
|
+
PKCE_MAX_CONCURRENT_FLOWS,
|
|
6
|
+
} from './constants'
|
|
2
7
|
import { AuthInvalidJwtError } from './errors'
|
|
3
8
|
import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
|
|
4
9
|
import { JwtHeader, JwtPayload, SupportedStorage, User } from './types'
|
|
@@ -301,20 +306,207 @@ export async function generatePKCEChallenge(verifier: string) {
|
|
|
301
306
|
return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
302
307
|
}
|
|
303
308
|
|
|
309
|
+
const PKCE_FLOW_ID_PATTERN = /^[a-zA-Z0-9_-]{8,64}$/
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Returns the flow id if it is a plausible flow id, `null` otherwise. Flow
|
|
313
|
+
* ids can arrive via URL parameters, so anything outside the expected shape
|
|
314
|
+
* is discarded before it is used to build a storage key.
|
|
315
|
+
*/
|
|
316
|
+
export function validatePKCEFlowId(flowId: unknown): string | null {
|
|
317
|
+
return typeof flowId === 'string' && PKCE_FLOW_ID_PATTERN.test(flowId) ? flowId : null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function generatePKCEFlowId(): string {
|
|
321
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
322
|
+
const bytes = new Uint8Array(16)
|
|
323
|
+
crypto.getRandomValues(bytes)
|
|
324
|
+
return Array.from(bytes, dec2hex).join('')
|
|
325
|
+
}
|
|
326
|
+
let flowId = ''
|
|
327
|
+
for (let i = 0; i < 32; i++) {
|
|
328
|
+
flowId += Math.floor(Math.random() * 16).toString(16)
|
|
329
|
+
}
|
|
330
|
+
return flowId
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Slot keys deliberately end in `-code-verifier`: @supabase/ssr's server
|
|
334
|
+
// cookie adapter only persists writes immediately for keys with that suffix
|
|
335
|
+
// (no auth event fires when a verifier is stored). They also contain no dot,
|
|
336
|
+
// because @supabase/ssr chunks oversized cookies as `<key>.<number>` and a
|
|
337
|
+
// dot-delimited key could be mistaken for a chunk of the fixed
|
|
338
|
+
// `-code-verifier` cookie and clobbered by its chunk management.
|
|
339
|
+
export const pkceVerifierSlotKey = (storageKey: string, flowId: string) =>
|
|
340
|
+
`${storageKey}-flow-${flowId}-code-verifier`
|
|
341
|
+
|
|
342
|
+
const pkceFlowIndexKey = (storageKey: string) => `${storageKey}-flows-code-verifier`
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Storage adapters cannot enumerate keys, so the ids of pending verifier
|
|
346
|
+
* slots are tracked in an index entry, oldest first. Index entries pass
|
|
347
|
+
* through the same validation as URL-provided flow ids: with cookie-based
|
|
348
|
+
* storage the index contents are no more trustworthy than a URL parameter.
|
|
349
|
+
*/
|
|
350
|
+
async function getPKCEFlowIndex(storage: SupportedStorage, storageKey: string): Promise<string[]> {
|
|
351
|
+
const index = await getItemAsync(storage, pkceFlowIndexKey(storageKey))
|
|
352
|
+
return Array.isArray(index)
|
|
353
|
+
? index.filter((id): id is string => validatePKCEFlowId(id) !== null)
|
|
354
|
+
: []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The index is read-modify-write without a lock: two concurrent starts (e.g.
|
|
359
|
+
* two tabs) can lose one index update. The losing flow still works — its slot
|
|
360
|
+
* is addressed directly by key — but its entry is missing from the index, so
|
|
361
|
+
* it escapes both ring eviction and removeAllPKCEVerifiers: the orphaned slot
|
|
362
|
+
* persists for the storage medium's lifetime (up to the cookie max age in
|
|
363
|
+
* cookie storage) and repeated races accumulate one orphan each. Accepted
|
|
364
|
+
* trade-off: locking every flow start is far more intrusive than the leak.
|
|
365
|
+
*/
|
|
366
|
+
export async function storePKCEVerifier(
|
|
367
|
+
storage: SupportedStorage,
|
|
368
|
+
storageKey: string,
|
|
369
|
+
flowId: string,
|
|
370
|
+
verifier: string,
|
|
371
|
+
onEvictFlow?: (evictedFlowId: string) => void
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
await setItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId), verifier)
|
|
374
|
+
|
|
375
|
+
const index = (await getPKCEFlowIndex(storage, storageKey)).filter((id) => id !== flowId)
|
|
376
|
+
index.push(flowId)
|
|
377
|
+
while (index.length > PKCE_MAX_CONCURRENT_FLOWS) {
|
|
378
|
+
const evicted = index.shift()!
|
|
379
|
+
await removeItemAsync(storage, pkceVerifierSlotKey(storageKey, evicted))
|
|
380
|
+
onEvictFlow?.(evicted)
|
|
381
|
+
}
|
|
382
|
+
await setItemAsync(storage, pkceFlowIndexKey(storageKey), index)
|
|
383
|
+
|
|
384
|
+
// Deprecation-window dual write: exchanges that cannot identify their flow
|
|
385
|
+
// (older SDK versions, redirects without the flow id parameter) read the
|
|
386
|
+
// fixed key, which mirrors the most recently started flow.
|
|
387
|
+
await setItemAsync(storage, `${storageKey}-code-verifier`, verifier)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Looks up the verifier for `flowId`. When a flow id is given, only that slot
|
|
392
|
+
* is consulted — deliberately no fallback to the fixed legacy key: submitting
|
|
393
|
+
* another flow's verifier would burn the single-use auth code, and the
|
|
394
|
+
* subsequent cleanup would delete a pending flow's only fallback. The legacy
|
|
395
|
+
* key is read only when no flow id is available at all.
|
|
396
|
+
*/
|
|
397
|
+
export async function retrievePKCEVerifier(
|
|
398
|
+
storage: SupportedStorage,
|
|
399
|
+
storageKey: string,
|
|
400
|
+
flowId: string | null
|
|
401
|
+
): Promise<{ verifier: string | null; flowId: string | null }> {
|
|
402
|
+
if (flowId) {
|
|
403
|
+
const verifier = await getItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId))
|
|
404
|
+
return { verifier: typeof verifier === 'string' ? verifier : null, flowId }
|
|
405
|
+
}
|
|
406
|
+
const verifier = await getItemAsync(storage, `${storageKey}-code-verifier`)
|
|
407
|
+
return { verifier: typeof verifier === 'string' ? verifier : null, flowId: null }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Removes a single flow's verifier. Never clears other flows' slots: with a
|
|
412
|
+
* `flowId` only that slot is deleted (plus the legacy fixed key when it holds
|
|
413
|
+
* the same verifier); without one, only the legacy fixed key is deleted.
|
|
414
|
+
*/
|
|
415
|
+
export async function removePKCEVerifier(
|
|
416
|
+
storage: SupportedStorage,
|
|
417
|
+
storageKey: string,
|
|
418
|
+
flowId: string | null
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const legacyKey = `${storageKey}-code-verifier`
|
|
421
|
+
if (!flowId) {
|
|
422
|
+
await removeItemAsync(storage, legacyKey)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const slotKey = pkceVerifierSlotKey(storageKey, flowId)
|
|
427
|
+
const slotValue = await getItemAsync(storage, slotKey)
|
|
428
|
+
await removeItemAsync(storage, slotKey)
|
|
429
|
+
|
|
430
|
+
// Skip the index rewrite when the flow was never indexed (e.g. a failed
|
|
431
|
+
// exchange for an absent slot): on cookie storage every write is a full
|
|
432
|
+
// Set-Cookie cycle.
|
|
433
|
+
const index = await getPKCEFlowIndex(storage, storageKey)
|
|
434
|
+
const remaining = index.filter((id) => id !== flowId)
|
|
435
|
+
if (remaining.length !== index.length) {
|
|
436
|
+
if (remaining.length > 0) {
|
|
437
|
+
await setItemAsync(storage, pkceFlowIndexKey(storageKey), remaining)
|
|
438
|
+
} else {
|
|
439
|
+
await removeItemAsync(storage, pkceFlowIndexKey(storageKey))
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (slotValue != null && slotValue === (await getItemAsync(storage, legacyKey))) {
|
|
444
|
+
await removeItemAsync(storage, legacyKey)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Removes every pending verifier: all slots in the index, the index itself
|
|
450
|
+
* and the fixed legacy key. Used on session teardown (sign-out, invalid
|
|
451
|
+
* session) — matches the pre-slot behavior where tearing down the session
|
|
452
|
+
* deleted the only verifier, and prevents long-lived stale verifier cookies.
|
|
453
|
+
*/
|
|
454
|
+
export async function removeAllPKCEVerifiers(
|
|
455
|
+
storage: SupportedStorage,
|
|
456
|
+
storageKey: string
|
|
457
|
+
): Promise<void> {
|
|
458
|
+
const index = await getPKCEFlowIndex(storage, storageKey)
|
|
459
|
+
for (const flowId of index) {
|
|
460
|
+
await removeItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId))
|
|
461
|
+
}
|
|
462
|
+
await removeItemAsync(storage, pkceFlowIndexKey(storageKey))
|
|
463
|
+
await removeItemAsync(storage, `${storageKey}-code-verifier`)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Appends the reserved flow id parameter to a `redirectTo` URL, replacing any
|
|
468
|
+
* existing occurrence. String-based (no URL round-trip) so custom schemes
|
|
469
|
+
* (native deep links) and the exact encoding of the app's own parameters
|
|
470
|
+
* survive untouched; an existing fragment stays at the end of the URL.
|
|
471
|
+
*/
|
|
472
|
+
export function appendFlowIdToRedirectTo(redirectTo: string, flowId: string): string {
|
|
473
|
+
const hashIndex = redirectTo.indexOf('#')
|
|
474
|
+
let base = hashIndex === -1 ? redirectTo : redirectTo.slice(0, hashIndex)
|
|
475
|
+
const fragment = hashIndex === -1 ? '' : redirectTo.slice(hashIndex)
|
|
476
|
+
|
|
477
|
+
const queryIndex = base.indexOf('?')
|
|
478
|
+
if (queryIndex !== -1) {
|
|
479
|
+
const path = base.slice(0, queryIndex)
|
|
480
|
+
const remaining = base
|
|
481
|
+
.slice(queryIndex + 1)
|
|
482
|
+
.split('&')
|
|
483
|
+
.filter(
|
|
484
|
+
(pair) =>
|
|
485
|
+
pair !== '' && pair !== PKCE_FLOW_ID_PARAM && !pair.startsWith(`${PKCE_FLOW_ID_PARAM}=`)
|
|
486
|
+
)
|
|
487
|
+
base = remaining.length > 0 ? `${path}?${remaining.join('&')}` : path
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const separator = base.includes('?') ? '&' : '?'
|
|
491
|
+
return `${base}${separator}${PKCE_FLOW_ID_PARAM}=${encodeURIComponent(flowId)}${fragment}`
|
|
492
|
+
}
|
|
493
|
+
|
|
304
494
|
export async function getCodeChallengeAndMethod(
|
|
305
495
|
storage: SupportedStorage,
|
|
306
496
|
storageKey: string,
|
|
307
|
-
isPasswordRecovery = false
|
|
308
|
-
)
|
|
497
|
+
isPasswordRecovery = false,
|
|
498
|
+
onEvictFlow?: (evictedFlowId: string) => void
|
|
499
|
+
): Promise<[string, string, string]> {
|
|
309
500
|
const codeVerifier = generatePKCEVerifier()
|
|
310
501
|
let storedCodeVerifier = codeVerifier
|
|
311
502
|
if (isPasswordRecovery) {
|
|
312
503
|
storedCodeVerifier += '/recovery'
|
|
313
504
|
}
|
|
314
|
-
|
|
505
|
+
const flowId = generatePKCEFlowId()
|
|
506
|
+
await storePKCEVerifier(storage, storageKey, flowId, storedCodeVerifier, onEvictFlow)
|
|
315
507
|
const codeChallenge = await generatePKCEChallenge(codeVerifier)
|
|
316
508
|
const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
|
|
317
|
-
return [codeChallenge, codeChallengeMethod]
|
|
509
|
+
return [codeChallenge, codeChallengeMethod, flowId]
|
|
318
510
|
}
|
|
319
511
|
|
|
320
512
|
/** Parses the API version which is 2YYY-MM-DD. */
|
package/src/lib/types.ts
CHANGED
|
@@ -190,6 +190,30 @@ export type ExperimentalFeatureFlags = {
|
|
|
190
190
|
* disabled throws a descriptive error at call time.
|
|
191
191
|
*/
|
|
192
192
|
passkey?: boolean
|
|
193
|
+
/**
|
|
194
|
+
* Appends a reserved `sb_flow_id` query parameter to `redirectTo` URLs on
|
|
195
|
+
* PKCE flows. The parameter round-trips through the auth server back to
|
|
196
|
+
* your callback URL, where the client uses it to select the code verifier
|
|
197
|
+
* created by that specific flow — so multiple sign-in flows (e.g. two OAuth
|
|
198
|
+
* providers started in different tabs) can be in flight at the same time
|
|
199
|
+
* without overwriting each other.
|
|
200
|
+
*
|
|
201
|
+
* Before enabling, make sure your [redirect URL allow
|
|
202
|
+
* list](https://supabase.com/docs/guides/auth/redirect-urls) tolerates the
|
|
203
|
+
* extra query parameter: allow-list entries are matched against the full
|
|
204
|
+
* URL including the query string, so an exact entry (no wildcard) stops
|
|
205
|
+
* matching once the parameter is appended and the redirect falls back to
|
|
206
|
+
* your Site URL. Redirects to the Site URL's own origin always pass.
|
|
207
|
+
*
|
|
208
|
+
* Defaults to `false`. Without it, concurrent flows still keep separate
|
|
209
|
+
* verifiers in storage, but no flow id travels through the redirect: to
|
|
210
|
+
* match a callback to its verifier you must carry the `flowId` returned by
|
|
211
|
+
* `signInWithOAuth` (or `linkIdentity`) through your own channel and pass
|
|
212
|
+
* it to `exchangeCodeForSession`. Flows that offer no way to obtain the
|
|
213
|
+
* flow id (email OTP, password recovery, sign-up confirmation) can only be
|
|
214
|
+
* correlated via this flag.
|
|
215
|
+
*/
|
|
216
|
+
appendPkceFlowIdToRedirects?: boolean
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
const WeakPasswordReasons = ['length', 'characters', 'pwned'] as const
|
|
@@ -275,6 +299,19 @@ export type OAuthResponse =
|
|
|
275
299
|
data: {
|
|
276
300
|
provider: Provider
|
|
277
301
|
url: string
|
|
302
|
+
/**
|
|
303
|
+
* Identifier of the PKCE flow started by this call, usable as the
|
|
304
|
+
* `flowId` option of {@link GoTrueClient#exchangeCodeForSession} to
|
|
305
|
+
* select this flow's code verifier when several flows are in flight.
|
|
306
|
+
* `null` on the implicit flow. The id is a selector for a verifier
|
|
307
|
+
* kept in storage — it is not a secret and never contains the
|
|
308
|
+
* verifier itself.
|
|
309
|
+
*
|
|
310
|
+
* Always set at runtime; optional in the type so existing code that
|
|
311
|
+
* constructs `OAuthResponse` values (e.g. test mocks) keeps
|
|
312
|
+
* compiling.
|
|
313
|
+
*/
|
|
314
|
+
flowId?: string | null
|
|
278
315
|
}
|
|
279
316
|
error: null
|
|
280
317
|
}
|
|
@@ -282,6 +319,7 @@ export type OAuthResponse =
|
|
|
282
319
|
data: {
|
|
283
320
|
provider: Provider
|
|
284
321
|
url: null
|
|
322
|
+
flowId?: string | null
|
|
285
323
|
}
|
|
286
324
|
error: AuthError
|
|
287
325
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// - Debugging and support (identifying which version is running)
|
|
5
5
|
// - Telemetry and logging (version reporting in errors/analytics)
|
|
6
6
|
// - Ensuring build artifacts match the published package version
|
|
7
|
-
export const version = '2.
|
|
7
|
+
export const version = '2.111.0'
|