• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / auth-js / 17979937733

24 Sep 2025 02:28PM UTC coverage: 69.764% (-10.0%) from 79.786%
17979937733

push

github

web-flow
feat: mfa with webauthn support (#1118)

## What kind of change does this PR introduce?

**Feature** - This PR introduces YubiKey support for Multi-Factor
Authentication (MFA) via WebAuthn, enabling users to authenticate with
hardware security keys.

## What is the current behavior?

Currently, Supabase Auth JS supports two MFA methods:
  - TOTP (Time-based One-Time Password) authenticators
  - SMS-based verification
 
## What is the new behavior?

This PR adds full WebAuthn support to the authentication library, the
defaults enable yubikey support at the moment, but it allows the user to
override some parameters client-side to use other types of passkey
methods.

The PR adds the 'webauthn' factor type, to `listFactors`, `enroll()`,
`challenge()`, and `verify()`

(De)serialization of the webauthn reponse/credential object is done
behind the scenes via dedicated objects.

it also adds a new `experimental` namespace `.mfa.webauthn` which has a
`.register()` and `.authenticate()` methods, these methods allows
**single click** yubikey 2FA addition with a single function call.

additionally, we have `webauthn.{enroll|challenge|verify}()`, which
abstract away some of the logic surrounding enrollment, interaction with
the verifier, and have defaults for factortype etc.

### Two ways to use the new api:
#### Single Step
```typescript
const { data, error } = await client.mfa.webauthn.register({
				friendlyName: `Security Key ${new Date().toLocaleDateString()}`,
				rpId: window.location.hostname,
				rpOrigins: [window.location.origin]
			}, {
				authenticatorSelection: {
					authenticatorAttachment: 'platform',
					residentKey: 'discouraged',
					userVerification: 'discouraged',
					requireResidentKey: false
				}
			});

			if (error) throw error;

			console.log(data); // <- session
```
#### Multi Step Composition
```typescript
const { enroll, challenge, verify } = new WebAuthnApi(client);
		return enroll({
			friendlyName: params.friendlyName
		})
		... (continued)

1070 of 1681 branches covered (63.65%)

Branch coverage included in aggregate %.

45 of 258 new or added lines in 5 files covered. (17.44%)

20 existing lines in 3 files now uncovered.

1475 of 1967 relevant lines covered (74.99%)

69.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.78
/src/GoTrueClient.ts
1
import GoTrueAdminApi from './GoTrueAdminApi'
5✔
2
import {
5✔
3
  AUTO_REFRESH_TICK_DURATION_MS,
4
  AUTO_REFRESH_TICK_THRESHOLD,
5
  DEFAULT_HEADERS,
6
  EXPIRY_MARGIN_MS,
7
  GOTRUE_URL,
8
  JWKS_TTL,
9
  STORAGE_KEY,
10
} from './lib/constants'
11
import {
5✔
12
  AuthError,
13
  AuthImplicitGrantRedirectError,
14
  AuthInvalidCredentialsError,
15
  AuthInvalidJwtError,
16
  AuthInvalidTokenResponseError,
17
  AuthPKCEGrantCodeExchangeError,
18
  AuthSessionMissingError,
19
  AuthUnknownError,
20
  isAuthApiError,
21
  isAuthError,
22
  isAuthImplicitGrantRedirectError,
23
  isAuthRetryableFetchError,
24
  isAuthSessionMissingError,
25
} from './lib/errors'
26
import {
5✔
27
  Fetch,
28
  _request,
29
  _sessionResponse,
30
  _sessionResponsePassword,
31
  _ssoResponse,
32
  _userResponse,
33
} from './lib/fetch'
34
import {
5✔
35
  decodeJWT,
36
  deepClone,
37
  Deferred,
38
  getAlgorithm,
39
  getCodeChallengeAndMethod,
40
  getItemAsync,
41
  isBrowser,
42
  parseParametersFromURL,
43
  removeItemAsync,
44
  resolveFetch,
45
  retryable,
46
  setItemAsync,
47
  sleep,
48
  supportsLocalStorage,
49
  userNotAvailableProxy,
50
  uuid,
51
  validateExp,
52
} from './lib/helpers'
53
import { memoryLocalStorageAdapter } from './lib/local-storage'
5✔
54
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
5✔
55
import { polyfillGlobalThis } from './lib/polyfills'
5✔
56
import { version } from './lib/version'
5✔
57

58
import { bytesToBase64URL, stringToUint8Array } from './lib/base64url'
5✔
59
import type {
60
  AuthChangeEvent,
61
  AuthenticatorAssuranceLevels,
62
  AuthFlowType,
63
  AuthMFAChallengePhoneResponse,
64
  AuthMFAChallengeResponse,
65
  AuthMFAChallengeTOTPResponse,
66
  AuthMFAChallengeWebauthnResponse,
67
  AuthMFAChallengeWebauthnServerResponse,
68
  AuthMFAEnrollPhoneResponse,
69
  AuthMFAEnrollResponse,
70
  AuthMFAEnrollTOTPResponse,
71
  AuthMFAEnrollWebauthnResponse,
72
  AuthMFAGetAuthenticatorAssuranceLevelResponse,
73
  AuthMFAListFactorsResponse,
74
  AuthMFAUnenrollResponse,
75
  AuthMFAVerifyResponse,
76
  AuthOtpResponse,
77
  AuthResponse,
78
  AuthResponsePassword,
79
  AuthTokenResponse,
80
  AuthTokenResponsePassword,
81
  CallRefreshTokenResult,
82
  EthereumWallet,
83
  EthereumWeb3Credentials,
84
  Factor,
85
  GoTrueClientOptions,
86
  GoTrueMFAApi,
87
  InitializeResult,
88
  JWK,
89
  JwtHeader,
90
  JwtPayload,
91
  LockFunc,
92
  MFAChallengeAndVerifyParams,
93
  MFAChallengeParams,
94
  MFAChallengePhoneParams,
95
  MFAChallengeTOTPParams,
96
  MFAChallengeWebauthnParams,
97
  MFAEnrollParams,
98
  MFAEnrollPhoneParams,
99
  MFAEnrollTOTPParams,
100
  MFAEnrollWebauthnParams,
101
  MFAUnenrollParams,
102
  MFAVerifyParams,
103
  MFAVerifyPhoneParams,
104
  MFAVerifyTOTPParams,
105
  MFAVerifyWebauthnParamFields,
106
  MFAVerifyWebauthnParams,
107
  OAuthResponse,
108
  Prettify,
109
  Provider,
110
  ResendParams,
111
  Session,
112
  SignInAnonymouslyCredentials,
113
  SignInWithIdTokenCredentials,
114
  SignInWithOAuthCredentials,
115
  SignInWithPasswordCredentials,
116
  SignInWithPasswordlessCredentials,
117
  SignInWithSSO,
118
  SignOut,
119
  SignUpWithPasswordCredentials,
120
  SolanaWallet,
121
  SolanaWeb3Credentials,
122
  SSOResponse,
123
  StrictOmit,
124
  Subscription,
125
  SupportedStorage,
126
  User,
127
  UserAttributes,
128
  UserIdentity,
129
  UserResponse,
130
  VerifyOtpParams,
131
  Web3Credentials,
132
} from './lib/types'
133
import {
5✔
134
  createSiweMessage,
135
  fromHex,
136
  getAddress,
137
  Hex,
138
  SiweMessage,
139
  toHex,
140
} from './lib/web3/ethereum'
141
import {
5✔
142
  deserializeCredentialCreationOptions,
143
  deserializeCredentialRequestOptions,
144
  serializeCredentialCreationResponse,
145
  serializeCredentialRequestResponse,
146
  WebAuthnApi,
147
} from './lib/webauthn'
148
import {
149
  AuthenticationCredential,
150
  PublicKeyCredentialJSON,
151
  RegistrationCredential,
152
} from './lib/webauthn.dom'
153

154
polyfillGlobalThis() // Make "globalThis" available
5✔
155

156
const DEFAULT_OPTIONS: Omit<
157
  Required<GoTrueClientOptions>,
158
  'fetch' | 'storage' | 'userStorage' | 'lock'
159
> = {
5✔
160
  url: GOTRUE_URL,
161
  storageKey: STORAGE_KEY,
162
  autoRefreshToken: true,
163
  persistSession: true,
164
  detectSessionInUrl: true,
165
  headers: DEFAULT_HEADERS,
166
  flowType: 'implicit',
167
  debug: false,
168
  hasCustomAuthorizationHeader: false,
169
}
170

171
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
172
  return await fn()
273✔
173
}
174

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

185
export default class GoTrueClient {
5✔
186
  private static nextInstanceID = 0
5✔
187

188
  private instanceID: number
189

190
  /**
191
   * Namespace for the GoTrue admin methods.
192
   * These methods should only be used in a trusted server-side environment.
193
   */
194
  admin: GoTrueAdminApi
195
  /**
196
   * Namespace for the MFA methods.
197
   */
198
  mfa: GoTrueMFAApi
199
  /**
200
   * The storage key used to identify the values saved in localStorage
201
   */
202
  protected storageKey: string
203

204
  protected flowType: AuthFlowType
205

206
  /**
207
   * The JWKS used for verifying asymmetric JWTs
208
   */
209
  protected get jwks() {
210
    return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
98✔
211
  }
212

213
  protected set jwks(value: { keys: JWK[] }) {
214
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
7✔
215
  }
216

217
  protected get jwks_cached_at() {
218
    return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
5✔
219
  }
220

221
  protected set jwks_cached_at(value: number) {
222
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
7✔
223
  }
224

225
  protected autoRefreshToken: boolean
226
  protected persistSession: boolean
227
  protected storage: SupportedStorage
228
  /**
229
   * @experimental
230
   */
231
  protected userStorage: SupportedStorage | null = null
91✔
232
  protected memoryStorage: { [key: string]: string } | null = null
91✔
233
  protected stateChangeEmitters: Map<string, Subscription> = new Map()
91✔
234
  protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
91✔
235
  protected visibilityChangedCallback: (() => Promise<any>) | null = null
91✔
236
  protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
91✔
237
  /**
238
   * Keeps track of the async client initialization.
239
   * When null or not yet resolved the auth state is `unknown`
240
   * Once resolved the auth state is known and it's safe to call any further client methods.
241
   * Keep extra care to never reject or throw uncaught errors
242
   */
243
  protected initializePromise: Promise<InitializeResult> | null = null
91✔
244
  protected detectSessionInUrl = true
91✔
245
  protected url: string
246
  protected headers: {
247
    [key: string]: string
248
  }
249
  protected hasCustomAuthorizationHeader = false
91✔
250
  protected suppressGetSessionWarning = false
91✔
251
  protected fetch: Fetch
252
  protected lock: LockFunc
253
  protected lockAcquired = false
91✔
254
  protected pendingInLock: Promise<any>[] = []
91✔
255

256
  /**
257
   * Used to broadcast state change events to other tabs listening.
258
   */
259
  protected broadcastChannel: BroadcastChannel | null = null
91✔
260

261
  protected logDebugMessages: boolean
262
  protected logger: (message: string, ...args: any[]) => void = console.log
91✔
263

264
  /**
265
   * Create a new client for use in the browser.
266
   */
267
  constructor(options: GoTrueClientOptions) {
268
    this.instanceID = GoTrueClient.nextInstanceID
91✔
269
    GoTrueClient.nextInstanceID += 1
91✔
270

271
    if (this.instanceID > 0 && isBrowser()) {
91✔
272
      console.warn(
36✔
273
        'Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.'
274
      )
275
    }
276

277
    const settings = { ...DEFAULT_OPTIONS, ...options }
91✔
278

279
    this.logDebugMessages = !!settings.debug
91✔
280
    if (typeof settings.debug === 'function') {
91✔
281
      this.logger = settings.debug
1✔
282
    }
283

284
    this.persistSession = settings.persistSession
91✔
285
    this.storageKey = settings.storageKey
91✔
286
    this.autoRefreshToken = settings.autoRefreshToken
91✔
287
    this.admin = new GoTrueAdminApi({
91✔
288
      url: settings.url,
289
      headers: settings.headers,
290
      fetch: settings.fetch,
291
    })
292

293
    this.url = settings.url
91✔
294
    this.headers = settings.headers
91✔
295
    this.fetch = resolveFetch(settings.fetch)
91✔
296
    this.lock = settings.lock || lockNoOp
91✔
297
    this.detectSessionInUrl = settings.detectSessionInUrl
91✔
298
    this.flowType = settings.flowType
91✔
299
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
91✔
300

301
    if (settings.lock) {
91✔
302
      this.lock = settings.lock
4✔
303
    } else if (isBrowser() && globalThis?.navigator?.locks) {
87!
304
      this.lock = navigatorLock
18✔
305
    } else {
306
      this.lock = lockNoOp
69✔
307
    }
308

309
    if (!this.jwks) {
91!
310
      this.jwks = { keys: [] }
×
311
      this.jwks_cached_at = Number.MIN_SAFE_INTEGER
×
312
    }
313

314
    this.mfa = {
91✔
315
      verify: this._verify.bind(this),
316
      enroll: this._enroll.bind(this),
317
      unenroll: this._unenroll.bind(this),
318
      challenge: this._challenge.bind(this),
319
      listFactors: this._listFactors.bind(this),
320
      challengeAndVerify: this._challengeAndVerify.bind(this),
321
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
322
      webauthn: new WebAuthnApi(this),
323
    }
324

325
    if (this.persistSession) {
91✔
326
      if (settings.storage) {
86✔
327
        this.storage = settings.storage
68✔
328
      } else {
329
        if (supportsLocalStorage()) {
18✔
330
          this.storage = globalThis.localStorage
1✔
331
        } else {
332
          this.memoryStorage = {}
17✔
333
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
17✔
334
        }
335
      }
336

337
      if (settings.userStorage) {
86✔
338
        this.userStorage = settings.userStorage
6✔
339
      }
340
    } else {
341
      this.memoryStorage = {}
5✔
342
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
5✔
343
    }
344

345
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
91✔
346
      try {
22✔
347
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
22✔
348
      } catch (e: any) {
349
        console.error(
21✔
350
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
351
          e
352
        )
353
      }
354

355
      this.broadcastChannel?.addEventListener('message', async (event) => {
22✔
356
        this._debug('received broadcast notification from other tab or client', event)
1✔
357

358
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
1✔
359
      })
360
    }
361

362
    this.initialize()
91✔
363
  }
364

365
  private _debug(...args: any[]): GoTrueClient {
366
    if (this.logDebugMessages) {
3,613✔
367
      this.logger(
21✔
368
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
369
        ...args
370
      )
371
    }
372

373
    return this
3,613✔
374
  }
375

376
  /**
377
   * Initializes the client session either from the url or from storage.
378
   * This method is automatically called when instantiating the client, but should also be called
379
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
380
   */
381
  async initialize(): Promise<InitializeResult> {
382
    if (this.initializePromise) {
119✔
383
      return await this.initializePromise
28✔
384
    }
385

386
    this.initializePromise = (async () => {
91✔
387
      return await this._acquireLock(-1, async () => {
91✔
388
        return await this._initialize()
88✔
389
      })
390
    })()
391

392
    return await this.initializePromise
91✔
393
  }
394

395
  /**
396
   * IMPORTANT:
397
   * 1. Never throw in this method, as it is called from the constructor
398
   * 2. Never return a session from this method as it would be cached over
399
   *    the whole lifetime of the client
400
   */
401
  private async _initialize(): Promise<InitializeResult> {
402
    try {
88✔
403
      const params = parseParametersFromURL(window.location.href)
88✔
404
      let callbackUrlType = 'none'
35✔
405
      if (this._isImplicitGrantCallback(params)) {
35✔
406
        callbackUrlType = 'implicit'
4✔
407
      } else if (await this._isPKCECallback(params)) {
31✔
408
        callbackUrlType = 'pkce'
1✔
409
      }
410

411
      /**
412
       * Attempt to get the session from the URL only if these conditions are fulfilled
413
       *
414
       * Note: If the URL isn't one of the callback url types (implicit or pkce),
415
       * then there could be an existing session so we don't want to prematurely remove it
416
       */
417
      if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
35✔
418
        const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
5✔
419
        if (error) {
3✔
420
          this._debug('#_initialize()', 'error detecting session from URL', error)
2✔
421

422
          if (isAuthImplicitGrantRedirectError(error)) {
2✔
423
            const errorCode = error.details?.code
2✔
424
            if (
2!
425
              errorCode === 'identity_already_exists' ||
6✔
426
              errorCode === 'identity_not_found' ||
427
              errorCode === 'single_identity_not_deletable'
428
            ) {
429
              return { error }
×
430
            }
431
          }
432

433
          // failed login attempt via url,
434
          // remove old session as in verifyOtp, signUp and signInWith*
435
          await this._removeSession()
2✔
436

437
          return { error }
2✔
438
        }
439

440
        const { session, redirectType } = data
1✔
441

442
        this._debug(
1✔
443
          '#_initialize()',
444
          'detected session in URL',
445
          session,
446
          'redirect type',
447
          redirectType
448
        )
449

450
        await this._saveSession(session)
1✔
451

452
        setTimeout(async () => {
1✔
453
          if (redirectType === 'recovery') {
1!
454
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
455
          } else {
456
            await this._notifyAllSubscribers('SIGNED_IN', session)
1✔
457
          }
458
        }, 0)
459

460
        return { error: null }
1✔
461
      }
462
      // no login attempt via callback url try to recover session from storage
463
      await this._recoverAndRefresh()
30✔
464
      return { error: null }
30✔
465
    } catch (error) {
466
      if (isAuthError(error)) {
55!
467
        return { error }
×
468
      }
469

470
      return {
55✔
471
        error: new AuthUnknownError('Unexpected error during initialization', error),
472
      }
473
    } finally {
474
      await this._handleVisibilityChange()
88✔
475
      this._debug('#_initialize()', 'end')
88✔
476
    }
477
  }
478

479
  /**
480
   * Creates a new anonymous user.
481
   *
482
   * @returns A session where the is_anonymous claim in the access token JWT set to true
483
   */
484
  async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
485
    try {
3✔
486
      const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
3✔
487
        headers: this.headers,
488
        body: {
489
          data: credentials?.options?.data ?? {},
27✔
490
          gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
18✔
491
        },
492
        xform: _sessionResponse,
493
      })
494
      const { data, error } = res
2✔
495

496
      if (error || !data) {
2!
497
        return { data: { user: null, session: null }, error: error }
×
498
      }
499
      const session: Session | null = data.session
2✔
500
      const user: User | null = data.user
2✔
501

502
      if (data.session) {
2✔
503
        await this._saveSession(data.session)
2✔
504
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
505
      }
506

507
      return { data: { user, session }, error: null }
2✔
508
    } catch (error) {
509
      if (isAuthError(error)) {
1✔
510
        return { data: { user: null, session: null }, error }
1✔
511
      }
512

513
      throw error
×
514
    }
515
  }
516

517
  /**
518
   * Creates a new user.
519
   *
520
   * Be aware that if a user account exists in the system you may get back an
521
   * error message that attempts to hide this information from the user.
522
   * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
523
   *
524
   * @returns A logged-in session if the server has "autoconfirm" ON
525
   * @returns A user if the server has "autoconfirm" OFF
526
   */
527
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
528
    try {
63✔
529
      let res: AuthResponse
530
      if ('email' in credentials) {
63✔
531
        const { email, password, options } = credentials
60✔
532
        let codeChallenge: string | null = null
60✔
533
        let codeChallengeMethod: string | null = null
60✔
534
        if (this.flowType === 'pkce') {
60✔
535
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
536
            this.storage,
537
            this.storageKey
538
          )
539
        }
540
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
60✔
541
          headers: this.headers,
542
          redirectTo: options?.emailRedirectTo,
180✔
543
          body: {
544
            email,
545
            password,
546
            data: options?.data ?? {},
360✔
547
            gotrue_meta_security: { captcha_token: options?.captchaToken },
180✔
548
            code_challenge: codeChallenge,
549
            code_challenge_method: codeChallengeMethod,
550
          },
551
          xform: _sessionResponse,
552
        })
553
      } else if ('phone' in credentials) {
3✔
554
        const { phone, password, options } = credentials
2✔
555
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
2✔
556
          headers: this.headers,
557
          body: {
558
            phone,
559
            password,
560
            data: options?.data ?? {},
12✔
561
            channel: options?.channel ?? 'sms',
12✔
562
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6✔
563
          },
564
          xform: _sessionResponse,
565
        })
566
      } else {
567
        throw new AuthInvalidCredentialsError(
1✔
568
          'You must provide either an email or phone number and a password'
569
        )
570
      }
571

572
      const { data, error } = res
56✔
573

574
      if (error || !data) {
56!
575
        return { data: { user: null, session: null }, error: error }
×
576
      }
577

578
      const session: Session | null = data.session
56✔
579
      const user: User | null = data.user
56✔
580

581
      if (data.session) {
56✔
582
        await this._saveSession(data.session)
55✔
583
        await this._notifyAllSubscribers('SIGNED_IN', session)
55✔
584
      }
585

586
      return { data: { user, session }, error: null }
56✔
587
    } catch (error) {
588
      if (isAuthError(error)) {
7✔
589
        return { data: { user: null, session: null }, error }
7✔
590
      }
591

592
      throw error
×
593
    }
594
  }
595

596
  /**
597
   * Log in an existing user with an email and password or phone and password.
598
   *
599
   * Be aware that you may get back an error message that will not distinguish
600
   * between the cases where the account does not exist or that the
601
   * email/phone and password combination is wrong or that the account can only
602
   * be accessed via social login.
603
   */
604
  async signInWithPassword(
605
    credentials: SignInWithPasswordCredentials
606
  ): Promise<AuthTokenResponsePassword> {
607
    try {
18✔
608
      let res: AuthResponsePassword
609
      if ('email' in credentials) {
18✔
610
        const { email, password, options } = credentials
14✔
611
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
14✔
612
          headers: this.headers,
613
          body: {
614
            email,
615
            password,
616
            gotrue_meta_security: { captcha_token: options?.captchaToken },
42✔
617
          },
618
          xform: _sessionResponsePassword,
619
        })
620
      } else if ('phone' in credentials) {
4✔
621
        const { phone, password, options } = credentials
3✔
622
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
3✔
623
          headers: this.headers,
624
          body: {
625
            phone,
626
            password,
627
            gotrue_meta_security: { captcha_token: options?.captchaToken },
9✔
628
          },
629
          xform: _sessionResponsePassword,
630
        })
631
      } else {
632
        throw new AuthInvalidCredentialsError(
1✔
633
          'You must provide either an email or phone number and a password'
634
        )
635
      }
636
      const { data, error } = res
14✔
637

638
      if (error) {
14!
639
        return { data: { user: null, session: null }, error }
×
640
      } else if (!data || !data.session || !data.user) {
14✔
641
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
1✔
642
      }
643
      if (data.session) {
13✔
644
        await this._saveSession(data.session)
13✔
645
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
13✔
646
      }
647
      return {
13✔
648
        data: {
649
          user: data.user,
650
          session: data.session,
651
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
13!
652
        },
653
        error,
654
      }
655
    } catch (error) {
656
      if (isAuthError(error)) {
4✔
657
        return { data: { user: null, session: null }, error }
4✔
658
      }
659
      throw error
×
660
    }
661
  }
662

663
  /**
664
   * Log in an existing user via a third-party provider.
665
   * This method supports the PKCE flow.
666
   */
667
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
668
    return await this._handleProviderSignIn(credentials.provider, {
10✔
669
      redirectTo: credentials.options?.redirectTo,
30✔
670
      scopes: credentials.options?.scopes,
30✔
671
      queryParams: credentials.options?.queryParams,
30✔
672
      skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
30✔
673
    })
674
  }
675

676
  /**
677
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
678
   */
679
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
680
    await this.initializePromise
2✔
681

682
    return this._acquireLock(-1, async () => {
2✔
683
      return this._exchangeCodeForSession(authCode)
2✔
684
    })
685
  }
686

687
  /**
688
   * Signs in a user by verifying a message signed by the user's private key.
689
   * Supports Ethereum (via Sign-In-With-Ethereum) & Solana (Sign-In-With-Solana) standards,
690
   * both of which derive from the EIP-4361 standard
691
   * With slight variation on Solana's side.
692
   * @reference https://eips.ethereum.org/EIPS/eip-4361
693
   */
694
  async signInWithWeb3(credentials: Web3Credentials): Promise<
695
    | {
696
        data: { session: Session; user: User }
697
        error: null
698
      }
699
    | { data: { session: null; user: null }; error: AuthError }
700
  > {
701
    const { chain } = credentials
18✔
702

703
    switch (chain) {
18✔
704
      case 'ethereum':
705
        return await this.signInWithEthereum(credentials)
10✔
706
      case 'solana':
707
        return await this.signInWithSolana(credentials)
6✔
708
      default:
709
        throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
2✔
710
    }
711
  }
712

713
  private async signInWithEthereum(
714
    credentials: EthereumWeb3Credentials
715
  ): Promise<
716
    | { data: { session: Session; user: User }; error: null }
717
    | { data: { session: null; user: null }; error: AuthError }
718
  > {
719
    // TODO: flatten type
720
    let message: string
721
    let signature: Hex
722

723
    if ('message' in credentials) {
10✔
724
      message = credentials.message
4✔
725
      signature = credentials.signature
4✔
726
    } else {
727
      const { chain, wallet, statement, options } = credentials
6✔
728

729
      let resolvedWallet: EthereumWallet
730

731
      if (!isBrowser()) {
6✔
732
        if (typeof wallet !== 'object' || !options?.url) {
4!
733
          throw new Error(
2✔
734
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
735
          )
736
        }
737

738
        resolvedWallet = wallet
2✔
739
      } else if (typeof wallet === 'object') {
2!
740
        resolvedWallet = wallet
2✔
741
      } else {
742
        const windowAny = window as any
×
743

744
        if (
×
745
          'ethereum' in windowAny &&
×
746
          typeof windowAny.ethereum === 'object' &&
747
          'request' in windowAny.ethereum &&
748
          typeof windowAny.ethereum.request === 'function'
749
        ) {
750
          resolvedWallet = windowAny.ethereum
×
751
        } else {
752
          throw new Error(
×
753
            `@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'ethereum', wallet: resolvedUserWallet }) instead.`
754
          )
755
        }
756
      }
757

758
      const url = new URL(options?.url ?? window.location.href)
4✔
759

760
      const accounts = await resolvedWallet
4✔
761
        .request({
762
          method: 'eth_requestAccounts',
763
        })
764
        .then((accs) => accs as string[])
2✔
765
        .catch(() => {
766
          throw new Error(
1✔
767
            `@supabase/auth-js: Wallet method eth_requestAccounts is missing or invalid`
768
          )
769
        })
770

771
      if (!accounts || accounts.length === 0) {
2✔
772
        throw new Error(
1✔
773
          `@supabase/auth-js: No accounts available. Please ensure the wallet is connected.`
774
        )
775
      }
776

777
      const address = getAddress(accounts[0])
1✔
778

779
      let chainId = options?.signInWithEthereum?.chainId
×
780
      if (!chainId) {
×
781
        const chainIdHex = await resolvedWallet.request({
×
782
          method: 'eth_chainId',
783
        })
784
        chainId = fromHex(chainIdHex as Hex)
×
785
      }
786

787
      const siweMessage: SiweMessage = {
×
788
        domain: url.host,
789
        address: address,
790
        statement: statement,
791
        uri: url.href,
792
        version: '1',
793
        chainId: chainId,
794
        nonce: options?.signInWithEthereum?.nonce,
×
795
        issuedAt: options?.signInWithEthereum?.issuedAt ?? new Date(),
×
796
        expirationTime: options?.signInWithEthereum?.expirationTime,
×
797
        notBefore: options?.signInWithEthereum?.notBefore,
×
798
        requestId: options?.signInWithEthereum?.requestId,
×
799
        resources: options?.signInWithEthereum?.resources,
×
800
      }
801

802
      message = createSiweMessage(siweMessage)
×
803

804
      // Sign message
805
      signature = (await resolvedWallet.request({
×
806
        method: 'personal_sign',
807
        params: [toHex(message), address],
808
      })) as Hex
809
    }
810

811
    try {
4✔
812
      const { data, error } = await _request(
4✔
813
        this.fetch,
814
        'POST',
815
        `${this.url}/token?grant_type=web3`,
816
        {
817
          headers: this.headers,
818
          body: {
819
            chain: 'ethereum',
820
            message,
821
            signature,
822
            ...(credentials.options?.captchaToken
16!
823
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
824
              : null),
825
          },
826
          xform: _sessionResponse,
827
        }
828
      )
829
      if (error) {
×
830
        throw error
×
831
      }
832
      if (!data || !data.session || !data.user) {
×
833
        return {
×
834
          data: { user: null, session: null },
835
          error: new AuthInvalidTokenResponseError(),
836
        }
837
      }
838
      if (data.session) {
×
839
        await this._saveSession(data.session)
×
840
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
841
      }
842
      return { data: { ...data }, error }
×
843
    } catch (error) {
844
      if (isAuthError(error)) {
4✔
845
        return { data: { user: null, session: null }, error }
4✔
846
      }
847

848
      throw error
×
849
    }
850
  }
851

852
  private async signInWithSolana(credentials: SolanaWeb3Credentials) {
853
    let message: string
854
    let signature: Uint8Array
855

856
    if ('message' in credentials) {
6✔
857
      message = credentials.message
1✔
858
      signature = credentials.signature
1✔
859
    } else {
860
      const { chain, wallet, statement, options } = credentials
5✔
861

862
      let resolvedWallet: SolanaWallet
863

864
      if (!isBrowser()) {
5✔
865
        if (typeof wallet !== 'object' || !options?.url) {
3!
866
          throw new Error(
1✔
867
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
868
          )
869
        }
870

871
        resolvedWallet = wallet
2✔
872
      } else if (typeof wallet === 'object') {
2!
873
        resolvedWallet = wallet
2✔
874
      } else {
875
        const windowAny = window as any
×
876

877
        if (
×
878
          'solana' in windowAny &&
×
879
          typeof windowAny.solana === 'object' &&
880
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
881
            ('signMessage' in windowAny.solana &&
882
              typeof windowAny.solana.signMessage === 'function'))
883
        ) {
884
          resolvedWallet = windowAny.solana
×
885
        } else {
886
          throw new Error(
×
887
            `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
888
          )
889
        }
890
      }
891

892
      const url = new URL(options?.url ?? window.location.href)
4✔
893

894
      if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
4!
895
        const output = await resolvedWallet.signIn({
×
896
          issuedAt: new Date().toISOString(),
897

898
          ...options?.signInWithSolana,
×
899

900
          // non-overridable properties
901
          version: '1',
902
          domain: url.host,
903
          uri: url.href,
904

905
          ...(statement ? { statement } : null),
×
906
        })
907

908
        let outputToProcess: any
909

910
        if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
×
911
          outputToProcess = output[0]
×
912
        } else if (
×
913
          output &&
×
914
          typeof output === 'object' &&
915
          'signedMessage' in output &&
916
          'signature' in output
917
        ) {
918
          outputToProcess = output
×
919
        } else {
920
          throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
×
921
        }
922

923
        if (
×
924
          'signedMessage' in outputToProcess &&
×
925
          'signature' in outputToProcess &&
926
          (typeof outputToProcess.signedMessage === 'string' ||
927
            outputToProcess.signedMessage instanceof Uint8Array) &&
928
          outputToProcess.signature instanceof Uint8Array
929
        ) {
930
          message =
×
931
            typeof outputToProcess.signedMessage === 'string'
×
932
              ? outputToProcess.signedMessage
933
              : new TextDecoder().decode(outputToProcess.signedMessage)
934
          signature = outputToProcess.signature
×
935
        } else {
936
          throw new Error(
×
937
            '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
938
          )
939
        }
940
      } else {
941
        if (
4✔
942
          !('signMessage' in resolvedWallet) ||
12✔
943
          typeof resolvedWallet.signMessage !== 'function' ||
944
          !('publicKey' in resolvedWallet) ||
945
          typeof resolvedWallet !== 'object' ||
946
          !resolvedWallet.publicKey ||
947
          !('toBase58' in resolvedWallet.publicKey) ||
948
          typeof resolvedWallet.publicKey.toBase58 !== 'function'
949
        ) {
950
          throw new Error(
3✔
951
            '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
952
          )
953
        }
954

955
        message = [
1✔
956
          `${url.host} wants you to sign in with your Solana account:`,
957
          resolvedWallet.publicKey.toBase58(),
958
          ...(statement ? ['', statement, ''] : ['']),
1!
959
          'Version: 1',
960
          `URI: ${url.href}`,
961
          `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
9!
962
          ...(options?.signInWithSolana?.notBefore
7!
963
            ? [`Not Before: ${options.signInWithSolana.notBefore}`]
964
            : []),
965
          ...(options?.signInWithSolana?.expirationTime
7!
966
            ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
967
            : []),
968
          ...(options?.signInWithSolana?.chainId
7!
969
            ? [`Chain ID: ${options.signInWithSolana.chainId}`]
970
            : []),
971
          ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
7!
972
          ...(options?.signInWithSolana?.requestId
7!
973
            ? [`Request ID: ${options.signInWithSolana.requestId}`]
974
            : []),
975
          ...(options?.signInWithSolana?.resources?.length
10!
976
            ? [
977
                'Resources',
978
                ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
×
979
              ]
980
            : []),
981
        ].join('\n')
982

983
        const maybeSignature = await resolvedWallet.signMessage(
1✔
984
          new TextEncoder().encode(message),
985
          'utf8'
986
        )
987

988
        if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
×
989
          throw new Error(
×
990
            '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
991
          )
992
        }
993

994
        signature = maybeSignature
×
995
      }
996
    }
997

998
    try {
1✔
999
      const { data, error } = await _request(
1✔
1000
        this.fetch,
1001
        'POST',
1002
        `${this.url}/token?grant_type=web3`,
1003
        {
1004
          headers: this.headers,
1005
          body: {
1006
            chain: 'solana',
1007
            message,
1008
            signature: bytesToBase64URL(signature),
1009

1010
            ...(credentials.options?.captchaToken
4!
1011
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
1012
              : null),
1013
          },
1014
          xform: _sessionResponse,
1015
        }
1016
      )
1017
      if (error) {
×
1018
        throw error
×
1019
      }
1020
      if (!data || !data.session || !data.user) {
×
1021
        return {
×
1022
          data: { user: null, session: null },
1023
          error: new AuthInvalidTokenResponseError(),
1024
        }
1025
      }
1026
      if (data.session) {
×
1027
        await this._saveSession(data.session)
×
1028
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1029
      }
1030
      return { data: { ...data }, error }
×
1031
    } catch (error) {
1032
      if (isAuthError(error)) {
1✔
1033
        return { data: { user: null, session: null }, error }
1✔
1034
      }
1035

1036
      throw error
×
1037
    }
1038
  }
1039

1040
  private async _exchangeCodeForSession(authCode: string): Promise<
1041
    | {
1042
        data: { session: Session; user: User; redirectType: string | null }
1043
        error: null
1044
      }
1045
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1046
  > {
1047
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
3✔
1048
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
3✔
1049

1050
    try {
3✔
1051
      const { data, error } = await _request(
3✔
1052
        this.fetch,
1053
        'POST',
1054
        `${this.url}/token?grant_type=pkce`,
1055
        {
1056
          headers: this.headers,
1057
          body: {
1058
            auth_code: authCode,
1059
            code_verifier: codeVerifier,
1060
          },
1061
          xform: _sessionResponse,
1062
        }
1063
      )
1064
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1065
      if (error) {
×
1066
        throw error
×
1067
      }
1068
      if (!data || !data.session || !data.user) {
×
1069
        return {
×
1070
          data: { user: null, session: null, redirectType: null },
1071
          error: new AuthInvalidTokenResponseError(),
1072
        }
1073
      }
1074
      if (data.session) {
×
1075
        await this._saveSession(data.session)
×
1076
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1077
      }
1078
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
1079
    } catch (error) {
1080
      if (isAuthError(error)) {
3✔
1081
        return { data: { user: null, session: null, redirectType: null }, error }
3✔
1082
      }
1083

1084
      throw error
×
1085
    }
1086
  }
1087

1088
  /**
1089
   * Allows signing in with an OIDC ID token. The authentication provider used
1090
   * should be enabled and configured.
1091
   */
1092
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
1093
    try {
3✔
1094
      const { options, provider, token, access_token, nonce } = credentials
3✔
1095

1096
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
3✔
1097
        headers: this.headers,
1098
        body: {
1099
          provider,
1100
          id_token: token,
1101
          access_token,
1102
          nonce,
1103
          gotrue_meta_security: { captcha_token: options?.captchaToken },
9✔
1104
        },
1105
        xform: _sessionResponse,
1106
      })
1107

1108
      const { data, error } = res
1✔
1109
      if (error) {
1!
1110
        return { data: { user: null, session: null }, error }
×
1111
      } else if (!data || !data.session || !data.user) {
1!
1112
        return {
1✔
1113
          data: { user: null, session: null },
1114
          error: new AuthInvalidTokenResponseError(),
1115
        }
1116
      }
1117
      if (data.session) {
×
1118
        await this._saveSession(data.session)
×
1119
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1120
      }
1121
      return { data, error }
×
1122
    } catch (error) {
1123
      if (isAuthError(error)) {
2✔
1124
        return { data: { user: null, session: null }, error }
2✔
1125
      }
1126
      throw error
×
1127
    }
1128
  }
1129

1130
  /**
1131
   * Log in a user using magiclink or a one-time password (OTP).
1132
   *
1133
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
1134
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
1135
   * If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
1136
   *
1137
   * Be aware that you may get back an error message that will not distinguish
1138
   * between the cases where the account does not exist or, that the account
1139
   * can only be accessed via social login.
1140
   *
1141
   * Do note that you will need to configure a Whatsapp sender on Twilio
1142
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
1143
   * channel is not supported on other providers
1144
   * at this time.
1145
   * This method supports PKCE when an email is passed.
1146
   */
1147
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
1148
    try {
8✔
1149
      if ('email' in credentials) {
8✔
1150
        const { email, options } = credentials
4✔
1151
        let codeChallenge: string | null = null
4✔
1152
        let codeChallengeMethod: string | null = null
4✔
1153
        if (this.flowType === 'pkce') {
4✔
1154
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1✔
1155
            this.storage,
1156
            this.storageKey
1157
          )
1158
        }
1159
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
4✔
1160
          headers: this.headers,
1161
          body: {
1162
            email,
1163
            data: options?.data ?? {},
24✔
1164
            create_user: options?.shouldCreateUser ?? true,
24✔
1165
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12✔
1166
            code_challenge: codeChallenge,
1167
            code_challenge_method: codeChallengeMethod,
1168
          },
1169
          redirectTo: options?.emailRedirectTo,
12✔
1170
        })
1171
        return { data: { user: null, session: null }, error }
2✔
1172
      }
1173
      if ('phone' in credentials) {
4✔
1174
        const { phone, options } = credentials
3✔
1175
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
3✔
1176
          headers: this.headers,
1177
          body: {
1178
            phone,
1179
            data: options?.data ?? {},
18✔
1180
            create_user: options?.shouldCreateUser ?? true,
18✔
1181
            gotrue_meta_security: { captcha_token: options?.captchaToken },
9✔
1182
            channel: options?.channel ?? 'sms',
18✔
1183
          },
1184
        })
1185
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
1186
      }
1187
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
1✔
1188
    } catch (error) {
1189
      if (isAuthError(error)) {
6✔
1190
        return { data: { user: null, session: null }, error }
6✔
1191
      }
1192

1193
      throw error
×
1194
    }
1195
  }
1196

1197
  /**
1198
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
1199
   */
1200
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
1201
    try {
5✔
1202
      let redirectTo: string | undefined = undefined
5✔
1203
      let captchaToken: string | undefined = undefined
5✔
1204
      if ('options' in params) {
5✔
1205
        redirectTo = params.options?.redirectTo
2!
1206
        captchaToken = params.options?.captchaToken
2!
1207
      }
1208
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
5✔
1209
        headers: this.headers,
1210
        body: {
1211
          ...params,
1212
          gotrue_meta_security: { captcha_token: captchaToken },
1213
        },
1214
        redirectTo,
1215
        xform: _sessionResponse,
1216
      })
1217

1218
      if (error) {
×
1219
        throw error
×
1220
      }
1221

1222
      if (!data) {
×
1223
        throw new Error('An error occurred on token verification.')
×
1224
      }
1225

1226
      const session: Session | null = data.session
×
1227
      const user: User = data.user
×
1228

1229
      if (session?.access_token) {
×
1230
        await this._saveSession(session as Session)
×
1231
        await this._notifyAllSubscribers(
×
1232
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
1233
          session
1234
        )
1235
      }
1236

1237
      return { data: { user, session }, error: null }
×
1238
    } catch (error) {
1239
      if (isAuthError(error)) {
5✔
1240
        return { data: { user: null, session: null }, error }
5✔
1241
      }
1242

1243
      throw error
×
1244
    }
1245
  }
1246

1247
  /**
1248
   * Attempts a single-sign on using an enterprise Identity Provider. A
1249
   * successful SSO attempt will redirect the current page to the identity
1250
   * provider authorization page. The redirect URL is implementation and SSO
1251
   * protocol specific.
1252
   *
1253
   * You can use it by providing a SSO domain. Typically you can extract this
1254
   * domain by asking users for their email address. If this domain is
1255
   * registered on the Auth instance the redirect will use that organization's
1256
   * currently active SSO Identity Provider for the login.
1257
   *
1258
   * If you have built an organization-specific login page, you can use the
1259
   * organization's SSO Identity Provider UUID directly instead.
1260
   */
1261
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1262
    try {
4✔
1263
      let codeChallenge: string | null = null
4✔
1264
      let codeChallengeMethod: string | null = null
4✔
1265
      if (this.flowType === 'pkce') {
4✔
1266
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
1267
          this.storage,
1268
          this.storageKey
1269
        )
1270
      }
1271

1272
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
4✔
1273
        body: {
1274
          ...('providerId' in params ? { provider_id: params.providerId } : null),
4✔
1275
          ...('domain' in params ? { domain: params.domain } : null),
4✔
1276
          redirect_to: params.options?.redirectTo ?? undefined,
24✔
1277
          ...(params?.options?.captchaToken
28!
1278
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1279
            : null),
1280
          skip_http_redirect: true, // fetch does not handle redirects
1281
          code_challenge: codeChallenge,
1282
          code_challenge_method: codeChallengeMethod,
1283
        },
1284
        headers: this.headers,
1285
        xform: _ssoResponse,
1286
      })
1287
    } catch (error) {
1288
      if (isAuthError(error)) {
4✔
1289
        return { data: null, error }
4✔
1290
      }
1291
      throw error
×
1292
    }
1293
  }
1294

1295
  /**
1296
   * Sends a reauthentication OTP to the user's email or phone number.
1297
   * Requires the user to be signed-in.
1298
   */
1299
  async reauthenticate(): Promise<AuthResponse> {
1300
    await this.initializePromise
2✔
1301

1302
    return await this._acquireLock(-1, async () => {
2✔
1303
      return await this._reauthenticate()
2✔
1304
    })
1305
  }
1306

1307
  private async _reauthenticate(): Promise<AuthResponse> {
1308
    try {
2✔
1309
      return await this._useSession(async (result) => {
2✔
1310
        const {
1311
          data: { session },
1312
          error: sessionError,
1313
        } = result
2✔
1314
        if (sessionError) throw sessionError
2!
1315
        if (!session) throw new AuthSessionMissingError()
2!
1316

1317
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
2✔
1318
          headers: this.headers,
1319
          jwt: session.access_token,
1320
        })
1321
        return { data: { user: null, session: null }, error }
1✔
1322
      })
1323
    } catch (error) {
1324
      if (isAuthError(error)) {
1✔
1325
        return { data: { user: null, session: null }, error }
1✔
1326
      }
1327
      throw error
×
1328
    }
1329
  }
1330

1331
  /**
1332
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1333
   */
1334
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1335
    try {
5✔
1336
      const endpoint = `${this.url}/resend`
5✔
1337
      if ('email' in credentials) {
5✔
1338
        const { email, type, options } = credentials
2✔
1339
        const { error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1340
          headers: this.headers,
1341
          body: {
1342
            email,
1343
            type,
1344
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1345
          },
1346
          redirectTo: options?.emailRedirectTo,
6!
1347
        })
1348
        return { data: { user: null, session: null }, error }
2✔
1349
      } else if ('phone' in credentials) {
3✔
1350
        const { phone, type, options } = credentials
2✔
1351
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1352
          headers: this.headers,
1353
          body: {
1354
            phone,
1355
            type,
1356
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1357
          },
1358
        })
1359
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
2!
1360
      }
1361
      throw new AuthInvalidCredentialsError(
1✔
1362
        'You must provide either an email or phone number and a type'
1363
      )
1364
    } catch (error) {
1365
      if (isAuthError(error)) {
1✔
1366
        return { data: { user: null, session: null }, error }
1✔
1367
      }
1368
      throw error
×
1369
    }
1370
  }
1371

1372
  /**
1373
   * Returns the session, refreshing it if necessary.
1374
   *
1375
   * The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out.
1376
   *
1377
   * **IMPORTANT:** This method loads values directly from the storage attached
1378
   * to the client. If that storage is based on request cookies for example,
1379
   * the values in it may not be authentic and therefore it's strongly advised
1380
   * against using this method and its results in such circumstances. A warning
1381
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1382
   */
1383
  async getSession() {
1384
    await this.initializePromise
37✔
1385

1386
    const result = await this._acquireLock(-1, async () => {
37✔
1387
      return this._useSession(async (result) => {
37✔
1388
        return result
35✔
1389
      })
1390
    })
1391

1392
    return result
35✔
1393
  }
1394

1395
  /**
1396
   * Acquires a global lock based on the storage key.
1397
   */
1398
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1399
    this._debug('#_acquireLock', 'begin', acquireTimeout)
302✔
1400

1401
    try {
302✔
1402
      if (this.lockAcquired) {
302✔
1403
        const last = this.pendingInLock.length
1!
1404
          ? this.pendingInLock[this.pendingInLock.length - 1]
1405
          : Promise.resolve()
1406

1407
        const result = (async () => {
1✔
1408
          await last
1✔
1409
          return await fn()
1✔
1410
        })()
1411

1412
        this.pendingInLock.push(
1✔
1413
          (async () => {
1414
            try {
1✔
1415
              await result
1✔
1416
            } catch (e: any) {
1417
              // we just care if it finished
1418
            }
1419
          })()
1420
        )
1421

1422
        return result
1✔
1423
      }
1424

1425
      return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
301✔
1426
        this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
297✔
1427

1428
        try {
297✔
1429
          this.lockAcquired = true
297✔
1430

1431
          const result = fn()
297✔
1432

1433
          this.pendingInLock.push(
297✔
1434
            (async () => {
1435
              try {
297✔
1436
                await result
297✔
1437
              } catch (e: any) {
1438
                // we just care if it finished
1439
              }
1440
            })()
1441
          )
1442

1443
          await result
297✔
1444

1445
          // keep draining the queue until there's nothing to wait on
1446
          while (this.pendingInLock.length) {
293✔
1447
            const waitOn = [...this.pendingInLock]
293✔
1448

1449
            await Promise.all(waitOn)
293✔
1450

1451
            this.pendingInLock.splice(0, waitOn.length)
293✔
1452
          }
1453

1454
          return await result
293✔
1455
        } finally {
1456
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
297✔
1457

1458
          this.lockAcquired = false
297✔
1459
        }
1460
      })
1461
    } finally {
1462
      this._debug('#_acquireLock', 'end')
302✔
1463
    }
1464
  }
1465

1466
  /**
1467
   * Use instead of {@link #getSession} inside the library. It is
1468
   * semantically usually what you want, as getting a session involves some
1469
   * processing afterwards that requires only one client operating on the
1470
   * session at once across multiple tabs or processes.
1471
   */
1472
  private async _useSession<R>(
1473
    fn: (
1474
      result:
1475
        | {
1476
            data: {
1477
              session: Session
1478
            }
1479
            error: null
1480
          }
1481
        | {
1482
            data: {
1483
              session: null
1484
            }
1485
            error: AuthError
1486
          }
1487
        | {
1488
            data: {
1489
              session: null
1490
            }
1491
            error: null
1492
          }
1493
    ) => Promise<R>
1494
  ): Promise<R> {
1495
    this._debug('#_useSession', 'begin')
216✔
1496

1497
    try {
216✔
1498
      // the use of __loadSession here is the only correct use of the function!
1499
      const result = await this.__loadSession()
216✔
1500

1501
      return await fn(result)
213✔
1502
    } finally {
1503
      this._debug('#_useSession', 'end')
216✔
1504
    }
1505
  }
1506

1507
  /**
1508
   * NEVER USE DIRECTLY!
1509
   *
1510
   * Always use {@link #_useSession}.
1511
   */
1512
  private async __loadSession(): Promise<
1513
    | {
1514
        data: {
1515
          session: Session
1516
        }
1517
        error: null
1518
      }
1519
    | {
1520
        data: {
1521
          session: null
1522
        }
1523
        error: AuthError
1524
      }
1525
    | {
1526
        data: {
1527
          session: null
1528
        }
1529
        error: null
1530
      }
1531
  > {
1532
    this._debug('#__loadSession()', 'begin')
216✔
1533

1534
    if (!this.lockAcquired) {
216✔
1535
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
15✔
1536
    }
1537

1538
    try {
216✔
1539
      let currentSession: Session | null = null
216✔
1540

1541
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
216✔
1542

1543
      this._debug('#getSession()', 'session from storage', maybeSession)
214✔
1544

1545
      if (maybeSession !== null) {
214✔
1546
        if (this._isValidSession(maybeSession)) {
94✔
1547
          currentSession = maybeSession
88✔
1548
        } else {
1549
          this._debug('#getSession()', 'session from storage is not valid')
6✔
1550
          await this._removeSession()
6✔
1551
        }
1552
      }
1553

1554
      if (!currentSession) {
213✔
1555
        return { data: { session: null }, error: null }
125✔
1556
      }
1557

1558
      // A session is considered expired before the access token _actually_
1559
      // expires. When the autoRefreshToken option is off (or when the tab is
1560
      // in the background), very eager users of getSession() -- like
1561
      // realtime-js -- might send a valid JWT which will expire by the time it
1562
      // reaches the server.
1563
      const hasExpired = currentSession.expires_at
88!
1564
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1565
        : false
1566

1567
      this._debug(
88✔
1568
        '#__loadSession()',
1569
        `session has${hasExpired ? '' : ' not'} expired`,
88✔
1570
        'expires_at',
1571
        currentSession.expires_at
1572
      )
1573

1574
      if (!hasExpired) {
88✔
1575
        if (this.userStorage) {
83✔
1576
          const maybeUser: { user?: User | null } | null = (await getItemAsync(
4✔
1577
            this.userStorage,
1578
            this.storageKey + '-user'
1579
          )) as any
1580

1581
          if (maybeUser?.user) {
4!
1582
            currentSession.user = maybeUser.user
×
1583
          } else {
1584
            currentSession.user = userNotAvailableProxy()
4✔
1585
          }
1586
        }
1587

1588
        if (this.storage.isServer && currentSession.user) {
83✔
1589
          let suppressWarning = this.suppressGetSessionWarning
7✔
1590
          const proxySession: Session = new Proxy(currentSession, {
7✔
1591
            get: (target: any, prop: string, receiver: any) => {
1592
              if (!suppressWarning && prop === 'user') {
12✔
1593
                // only show warning when the user object is being accessed from the server
1594
                console.warn(
1✔
1595
                  'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
1596
                )
1597
                suppressWarning = true // keeps this proxy instance from logging additional warnings
1✔
1598
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
1✔
1599
              }
1600
              return Reflect.get(target, prop, receiver)
12✔
1601
            },
1602
          })
1603
          currentSession = proxySession
7✔
1604
        }
1605

1606
        return { data: { session: currentSession }, error: null }
83✔
1607
      }
1608

1609
      const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
5✔
1610
      if (error) {
5✔
1611
        return { data: { session: null }, error }
3✔
1612
      }
1613

1614
      return { data: { session }, error: null }
2✔
1615
    } finally {
1616
      this._debug('#__loadSession()', 'end')
216✔
1617
    }
1618
  }
1619

1620
  /**
1621
   * Gets the current user details if there is an existing session. This method
1622
   * performs a network request to the Supabase Auth server, so the returned
1623
   * value is authentic and can be used to base authorization rules on.
1624
   *
1625
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1626
   */
1627
  async getUser(jwt?: string): Promise<UserResponse> {
1628
    if (jwt) {
12✔
1629
      return await this._getUser(jwt)
4✔
1630
    }
1631

1632
    await this.initializePromise
8✔
1633

1634
    const result = await this._acquireLock(-1, async () => {
8✔
1635
      return await this._getUser()
8✔
1636
    })
1637

1638
    return result
8✔
1639
  }
1640

1641
  private async _getUser(jwt?: string): Promise<UserResponse> {
1642
    try {
19✔
1643
      if (jwt) {
19✔
1644
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
11✔
1645
          headers: this.headers,
1646
          jwt: jwt,
1647
          xform: _userResponse,
1648
        })
1649
      }
1650

1651
      return await this._useSession(async (result) => {
8✔
1652
        const { data, error } = result
8✔
1653
        if (error) {
8!
1654
          throw error
×
1655
        }
1656

1657
        // returns an error if there is no access_token or custom authorization header
1658
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
8✔
1659
          return { data: { user: null }, error: new AuthSessionMissingError() }
3✔
1660
        }
1661

1662
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
5✔
1663
          headers: this.headers,
1664
          jwt: data.session?.access_token ?? undefined,
30!
1665
          xform: _userResponse,
1666
        })
1667
      })
1668
    } catch (error) {
1669
      if (isAuthError(error)) {
5✔
1670
        if (isAuthSessionMissingError(error)) {
1!
1671
          // JWT contains a `session_id` which does not correspond to an active
1672
          // session in the database, indicating the user is signed out.
1673

1674
          await this._removeSession()
×
1675
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1676
        }
1677

1678
        return { data: { user: null }, error }
1✔
1679
      }
1680

1681
      throw error
4✔
1682
    }
1683
  }
1684

1685
  /**
1686
   * Updates user data for a logged in user.
1687
   */
1688
  async updateUser(
1689
    attributes: UserAttributes,
1690
    options: {
5✔
1691
      emailRedirectTo?: string | undefined
1692
    } = {}
1693
  ): Promise<UserResponse> {
1694
    await this.initializePromise
5✔
1695

1696
    return await this._acquireLock(-1, async () => {
5✔
1697
      return await this._updateUser(attributes, options)
5✔
1698
    })
1699
  }
1700

1701
  protected async _updateUser(
1702
    attributes: UserAttributes,
1703
    options: {
×
1704
      emailRedirectTo?: string | undefined
1705
    } = {}
1706
  ): Promise<UserResponse> {
1707
    try {
5✔
1708
      return await this._useSession(async (result) => {
5✔
1709
        const { data: sessionData, error: sessionError } = result
5✔
1710
        if (sessionError) {
5!
1711
          throw sessionError
×
1712
        }
1713
        if (!sessionData.session) {
5✔
1714
          throw new AuthSessionMissingError()
1✔
1715
        }
1716
        const session: Session = sessionData.session
4✔
1717
        let codeChallenge: string | null = null
4✔
1718
        let codeChallengeMethod: string | null = null
4✔
1719
        if (this.flowType === 'pkce' && attributes.email != null) {
4✔
1720
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1✔
1721
            this.storage,
1722
            this.storageKey
1723
          )
1724
        }
1725

1726
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
4✔
1727
          headers: this.headers,
1728
          redirectTo: options?.emailRedirectTo,
12!
1729
          body: {
1730
            ...attributes,
1731
            code_challenge: codeChallenge,
1732
            code_challenge_method: codeChallengeMethod,
1733
          },
1734
          jwt: session.access_token,
1735
          xform: _userResponse,
1736
        })
1737
        if (userError) throw userError
4!
1738
        session.user = data.user as User
4✔
1739
        await this._saveSession(session)
4✔
1740
        await this._notifyAllSubscribers('USER_UPDATED', session)
4✔
1741
        return { data: { user: session.user }, error: null }
4✔
1742
      })
1743
    } catch (error) {
1744
      if (isAuthError(error)) {
1✔
1745
        return { data: { user: null }, error }
1✔
1746
      }
1747

1748
      throw error
×
1749
    }
1750
  }
1751

1752
  /**
1753
   * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
1754
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1755
   * @param currentSession The current session that minimally contains an access token and refresh token.
1756
   */
1757
  async setSession(currentSession: {
1758
    access_token: string
1759
    refresh_token: string
1760
  }): Promise<AuthResponse> {
1761
    await this.initializePromise
6✔
1762

1763
    return await this._acquireLock(-1, async () => {
6✔
1764
      return await this._setSession(currentSession)
6✔
1765
    })
1766
  }
1767

1768
  protected async _setSession(currentSession: {
1769
    access_token: string
1770
    refresh_token: string
1771
  }): Promise<AuthResponse> {
1772
    try {
6✔
1773
      if (!currentSession.access_token || !currentSession.refresh_token) {
6✔
1774
        throw new AuthSessionMissingError()
2✔
1775
      }
1776

1777
      const timeNow = Date.now() / 1000
4✔
1778
      let expiresAt = timeNow
4✔
1779
      let hasExpired = true
4✔
1780
      let session: Session | null = null
4✔
1781
      const { payload } = decodeJWT(currentSession.access_token)
4✔
1782
      if (payload.exp) {
2✔
1783
        expiresAt = payload.exp
2✔
1784
        hasExpired = expiresAt <= timeNow
2✔
1785
      }
1786

1787
      if (hasExpired) {
2✔
1788
        const { data: refreshedSession, error } = await this._callRefreshToken(
1✔
1789
          currentSession.refresh_token
1790
        )
1791
        if (error) {
1✔
1792
          return { data: { user: null, session: null }, error: error }
1✔
1793
        }
1794

1795
        if (!refreshedSession) {
×
1796
          return { data: { user: null, session: null }, error: null }
×
1797
        }
1798
        session = refreshedSession
×
1799
      } else {
1800
        const { data, error } = await this._getUser(currentSession.access_token)
1✔
1801
        if (error) {
1!
1802
          throw error
×
1803
        }
1804
        session = {
1✔
1805
          access_token: currentSession.access_token,
1806
          refresh_token: currentSession.refresh_token,
1807
          user: data.user,
1808
          token_type: 'bearer',
1809
          expires_in: expiresAt - timeNow,
1810
          expires_at: expiresAt,
1811
        }
1812
        await this._saveSession(session)
1✔
1813
        await this._notifyAllSubscribers('SIGNED_IN', session)
1✔
1814
      }
1815

1816
      return { data: { user: session.user, session }, error: null }
1✔
1817
    } catch (error) {
1818
      if (isAuthError(error)) {
4✔
1819
        return { data: { session: null, user: null }, error }
4✔
1820
      }
1821

1822
      throw error
×
1823
    }
1824
  }
1825

1826
  /**
1827
   * Returns a new session, regardless of expiry status.
1828
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1829
   * If the current session's refresh token is invalid, an error will be thrown.
1830
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1831
   */
1832
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1833
    await this.initializePromise
4✔
1834

1835
    return await this._acquireLock(-1, async () => {
4✔
1836
      return await this._refreshSession(currentSession)
4✔
1837
    })
1838
  }
1839

1840
  protected async _refreshSession(currentSession?: {
1841
    refresh_token: string
1842
  }): Promise<AuthResponse> {
1843
    try {
4✔
1844
      return await this._useSession(async (result) => {
4✔
1845
        if (!currentSession) {
4✔
1846
          const { data, error } = result
2✔
1847
          if (error) {
2!
1848
            throw error
×
1849
          }
1850

1851
          currentSession = data.session ?? undefined
2✔
1852
        }
1853

1854
        if (!currentSession?.refresh_token) {
4✔
1855
          throw new AuthSessionMissingError()
1✔
1856
        }
1857

1858
        const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
3✔
1859
        if (error) {
3✔
1860
          return { data: { user: null, session: null }, error: error }
1✔
1861
        }
1862

1863
        if (!session) {
2!
1864
          return { data: { user: null, session: null }, error: null }
×
1865
        }
1866

1867
        return { data: { user: session.user, session }, error: null }
2✔
1868
      })
1869
    } catch (error) {
1870
      if (isAuthError(error)) {
1✔
1871
        return { data: { user: null, session: null }, error }
1✔
1872
      }
1873

1874
      throw error
×
1875
    }
1876
  }
1877

1878
  /**
1879
   * Gets the session data from a URL string
1880
   */
1881
  private async _getSessionFromURL(
1882
    params: { [parameter: string]: string },
1883
    callbackUrlType: string
1884
  ): Promise<
1885
    | {
1886
        data: { session: Session; redirectType: string | null }
1887
        error: null
1888
      }
1889
    | { data: { session: null; redirectType: null }; error: AuthError }
1890
  > {
1891
    try {
6✔
1892
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
6✔
1893

1894
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1895
      if (params.error || params.error_description || params.error_code) {
5✔
1896
        // The error class returned implies that the redirect is from an implicit grant flow
1897
        // but it could also be from a redirect error from a PKCE flow.
1898
        throw new AuthImplicitGrantRedirectError(
1✔
1899
          params.error_description || 'Error in URL with unspecified error_description',
1!
1900
          {
1901
            error: params.error || 'unspecified_error',
1!
1902
            code: params.error_code || 'unspecified_code',
2✔
1903
          }
1904
        )
1905
      }
1906

1907
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1908
      switch (callbackUrlType) {
4!
1909
        case 'implicit':
1910
          if (this.flowType === 'pkce') {
3!
1911
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1912
          }
1913
          break
3✔
1914
        case 'pkce':
1915
          if (this.flowType === 'implicit') {
1✔
1916
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
1✔
1917
          }
1918
          break
×
1919
        default:
1920
        // there's no mismatch so we continue
1921
      }
1922

1923
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1924
      if (callbackUrlType === 'pkce') {
3!
1925
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1926
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
1927
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1928
        if (error) throw error
×
1929

1930
        const url = new URL(window.location.href)
×
1931
        url.searchParams.delete('code')
×
1932

1933
        window.history.replaceState(window.history.state, '', url.toString())
×
1934

1935
        return { data: { session: data.session, redirectType: null }, error: null }
×
1936
      }
1937

1938
      const {
1939
        provider_token,
1940
        provider_refresh_token,
1941
        access_token,
1942
        refresh_token,
1943
        expires_in,
1944
        expires_at,
1945
        token_type,
1946
      } = params
3✔
1947

1948
      if (!access_token || !expires_in || !refresh_token || !token_type) {
3!
1949
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1950
      }
1951

1952
      const timeNow = Math.round(Date.now() / 1000)
3✔
1953
      const expiresIn = parseInt(expires_in)
3✔
1954
      let expiresAt = timeNow + expiresIn
3✔
1955

1956
      if (expires_at) {
3✔
1957
        expiresAt = parseInt(expires_at)
1✔
1958
      }
1959

1960
      const actuallyExpiresIn = expiresAt - timeNow
3✔
1961
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
3✔
1962
        console.warn(
1✔
1963
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1964
        )
1965
      }
1966

1967
      const issuedAt = expiresAt - expiresIn
3✔
1968
      if (timeNow - issuedAt >= 120) {
3✔
1969
        console.warn(
1✔
1970
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1971
          issuedAt,
1972
          expiresAt,
1973
          timeNow
1974
        )
1975
      } else if (timeNow - issuedAt < 0) {
2!
1976
        console.warn(
×
1977
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1978
          issuedAt,
1979
          expiresAt,
1980
          timeNow
1981
        )
1982
      }
1983

1984
      const { data, error } = await this._getUser(access_token)
3✔
1985
      if (error) throw error
1!
1986

1987
      const session: Session = {
1✔
1988
        provider_token,
1989
        provider_refresh_token,
1990
        access_token,
1991
        expires_in: expiresIn,
1992
        expires_at: expiresAt,
1993
        refresh_token,
1994
        token_type: token_type as 'bearer',
1995
        user: data.user,
1996
      }
1997

1998
      // Remove tokens from URL
1999
      window.location.hash = ''
1✔
2000
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
1✔
2001

2002
      return { data: { session, redirectType: params.type }, error: null }
1✔
2003
    } catch (error) {
2004
      if (isAuthError(error)) {
5✔
2005
        return { data: { session: null, redirectType: null }, error }
3✔
2006
      }
2007

2008
      throw error
2✔
2009
    }
2010
  }
2011

2012
  /**
2013
   * Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
2014
   */
2015
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
2016
    return Boolean(params.access_token || params.error_description)
36✔
2017
  }
2018

2019
  /**
2020
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
2021
   */
2022
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
2023
    const currentStorageContent = await getItemAsync(
32✔
2024
      this.storage,
2025
      `${this.storageKey}-code-verifier`
2026
    )
2027

2028
    return !!(params.code && currentStorageContent)
32✔
2029
  }
2030

2031
  /**
2032
   * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
2033
   *
2034
   * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
2035
   * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
2036
   *
2037
   * If using `others` scope, no `SIGNED_OUT` event is fired!
2038
   */
2039
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
115✔
2040
    await this.initializePromise
116✔
2041

2042
    return await this._acquireLock(-1, async () => {
116✔
2043
      return await this._signOut(options)
116✔
2044
    })
2045
  }
2046

2047
  protected async _signOut(
2048
    { scope }: SignOut = { scope: 'global' }
×
2049
  ): Promise<{ error: AuthError | null }> {
2050
    return await this._useSession(async (result) => {
116✔
2051
      const { data, error: sessionError } = result
115✔
2052
      if (sessionError) {
115!
2053
        return { error: sessionError }
×
2054
      }
2055
      const accessToken = data.session?.access_token
115✔
2056
      if (accessToken) {
115✔
2057
        const { error } = await this.admin.signOut(accessToken, scope)
26✔
2058
        if (error) {
25✔
2059
          // ignore 404s since user might not exist anymore
2060
          // ignore 401s since an invalid or expired JWT should sign out the current session
2061
          if (
1!
2062
            !(
2063
              isAuthApiError(error) &&
4✔
2064
              (error.status === 404 || error.status === 401 || error.status === 403)
2065
            )
2066
          ) {
2067
            return { error }
×
2068
          }
2069
        }
2070
      }
2071
      if (scope !== 'others') {
114✔
2072
        await this._removeSession()
114✔
2073
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
114✔
2074
      }
2075
      return { error: null }
114✔
2076
    })
2077
  }
2078

2079
  /**
2080
   * Receive a notification every time an auth event happens.
2081
   * @param callback A callback function to be invoked when an auth event happens.
2082
   */
2083
  onAuthStateChange(
2084
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
2085
  ): {
2086
    data: { subscription: Subscription }
2087
  } {
2088
    const id: string = uuid()
5✔
2089
    const subscription: Subscription = {
5✔
2090
      id,
2091
      callback,
2092
      unsubscribe: () => {
2093
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
5✔
2094

2095
        this.stateChangeEmitters.delete(id)
5✔
2096
      },
2097
    }
2098

2099
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
5✔
2100

2101
    this.stateChangeEmitters.set(id, subscription)
5✔
2102
    ;(async () => {
5✔
2103
      await this.initializePromise
5✔
2104

2105
      await this._acquireLock(-1, async () => {
5✔
2106
        this._emitInitialSession(id)
5✔
2107
      })
2108
    })()
2109

2110
    return { data: { subscription } }
5✔
2111
  }
2112

2113
  private async _emitInitialSession(id: string): Promise<void> {
2114
    return await this._useSession(async (result) => {
5✔
2115
      try {
5✔
2116
        const {
2117
          data: { session },
2118
          error,
2119
        } = result
5✔
2120
        if (error) throw error
5!
2121

2122
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
5✔
2123
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
5✔
2124
      } catch (err) {
2125
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
2126
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
2127
        console.error(err)
×
2128
      }
2129
    })
2130
  }
2131

2132
  /**
2133
   * Sends a password reset request to an email address. This method supports the PKCE flow.
2134
   *
2135
   * @param email The email address of the user.
2136
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
2137
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
2138
   */
2139
  async resetPasswordForEmail(
2140
    email: string,
2141
    options: {
×
2142
      redirectTo?: string
2143
      captchaToken?: string
2144
    } = {}
2145
  ): Promise<
2146
    | {
2147
        data: {}
2148
        error: null
2149
      }
2150
    | { data: null; error: AuthError }
2151
  > {
2152
    let codeChallenge: string | null = null
2✔
2153
    let codeChallengeMethod: string | null = null
2✔
2154

2155
    if (this.flowType === 'pkce') {
2!
2156
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
2157
        this.storage,
2158
        this.storageKey,
2159
        true // isPasswordRecovery
2160
      )
2161
    }
2162
    try {
2✔
2163
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
2✔
2164
        body: {
2165
          email,
2166
          code_challenge: codeChallenge,
2167
          code_challenge_method: codeChallengeMethod,
2168
          gotrue_meta_security: { captcha_token: options.captchaToken },
2169
        },
2170
        headers: this.headers,
2171
        redirectTo: options.redirectTo,
2172
      })
2173
    } catch (error) {
2174
      if (isAuthError(error)) {
×
2175
        return { data: null, error }
×
2176
      }
2177

2178
      throw error
×
2179
    }
2180
  }
2181

2182
  /**
2183
   * Gets all the identities linked to a user.
2184
   */
2185
  async getUserIdentities(): Promise<
2186
    | {
2187
        data: {
2188
          identities: UserIdentity[]
2189
        }
2190
        error: null
2191
      }
2192
    | { data: null; error: AuthError }
2193
  > {
2194
    try {
3✔
2195
      const { data, error } = await this.getUser()
3✔
2196
      if (error) throw error
3✔
2197
      return { data: { identities: data.user.identities ?? [] }, error: null }
2!
2198
    } catch (error) {
2199
      if (isAuthError(error)) {
1✔
2200
        return { data: null, error }
1✔
2201
      }
2202
      throw error
×
2203
    }
2204
  }
2205

2206
  /**
2207
   * Links an oauth identity to an existing user.
2208
   * This method supports the PKCE flow.
2209
   */
2210
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse>
2211

2212
  /**
2213
   * Links an OIDC identity to an existing user.
2214
   */
2215
  async linkIdentity(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse>
2216

2217
  async linkIdentity(credentials: any): Promise<any> {
2218
    if ('token' in credentials) {
2!
2219
      return this.linkIdentityIdToken(credentials)
×
2220
    }
2221

2222
    return this.linkIdentityOAuth(credentials)
2✔
2223
  }
2224

2225
  private async linkIdentityOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2226
    try {
2✔
2227
      const { data, error } = await this._useSession(async (result) => {
2✔
2228
        const { data, error } = result
2✔
2229
        if (error) throw error
2!
2230
        const url: string = await this._getUrlForProvider(
2✔
2231
          `${this.url}/user/identities/authorize`,
2232
          credentials.provider,
2233
          {
2234
            redirectTo: credentials.options?.redirectTo,
6!
2235
            scopes: credentials.options?.scopes,
6!
2236
            queryParams: credentials.options?.queryParams,
6!
2237
            skipBrowserRedirect: true,
2238
          }
2239
        )
2240
        return await _request(this.fetch, 'GET', url, {
2✔
2241
          headers: this.headers,
2242
          jwt: data.session?.access_token ?? undefined,
11✔
2243
        })
2244
      })
2245
      if (error) throw error
1!
2246
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
1!
2247
        window.location.assign(data?.url)
1!
2248
      }
2249
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
1!
2250
    } catch (error) {
2251
      if (isAuthError(error)) {
1✔
2252
        return { data: { provider: credentials.provider, url: null }, error }
1✔
2253
      }
2254
      throw error
×
2255
    }
2256
  }
2257

2258
  private async linkIdentityIdToken(
2259
    credentials: SignInWithIdTokenCredentials
2260
  ): Promise<AuthTokenResponse> {
2261
    return await this._useSession(async (result) => {
×
2262
      try {
×
2263
        const {
2264
          error: sessionError,
2265
          data: { session },
2266
        } = result
×
2267
        if (sessionError) throw sessionError
×
2268

2269
        const { options, provider, token, access_token, nonce } = credentials
×
2270

2271
        const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
×
2272
          headers: this.headers,
2273
          jwt: session?.access_token ?? undefined,
×
2274
          body: {
2275
            provider,
2276
            id_token: token,
2277
            access_token,
2278
            nonce,
2279
            link_identity: true,
2280
            gotrue_meta_security: { captcha_token: options?.captchaToken },
×
2281
          },
2282
          xform: _sessionResponse,
2283
        })
2284

2285
        const { data, error } = res
×
2286
        if (error) {
×
2287
          return { data: { user: null, session: null }, error }
×
2288
        } else if (!data || !data.session || !data.user) {
×
2289
          return {
×
2290
            data: { user: null, session: null },
2291
            error: new AuthInvalidTokenResponseError(),
2292
          }
2293
        }
2294
        if (data.session) {
×
2295
          await this._saveSession(data.session)
×
2296
          await this._notifyAllSubscribers('USER_UPDATED', data.session)
×
2297
        }
2298
        return { data, error }
×
2299
      } catch (error) {
2300
        if (isAuthError(error)) {
×
2301
          return { data: { user: null, session: null }, error }
×
2302
        }
2303
        throw error
×
2304
      }
2305
    })
2306
  }
2307

2308
  /**
2309
   * Unlinks an identity from a user by deleting it. The user will no longer be able to sign in with that identity once it's unlinked.
2310
   */
2311
  async unlinkIdentity(identity: UserIdentity): Promise<
2312
    | {
2313
        data: {}
2314
        error: null
2315
      }
2316
    | { data: null; error: AuthError }
2317
  > {
2318
    try {
1✔
2319
      return await this._useSession(async (result) => {
1✔
2320
        const { data, error } = result
1✔
2321
        if (error) {
1!
2322
          throw error
×
2323
        }
2324
        return await _request(
1✔
2325
          this.fetch,
2326
          'DELETE',
2327
          `${this.url}/user/identities/${identity.identity_id}`,
2328
          {
2329
            headers: this.headers,
2330
            jwt: data.session?.access_token ?? undefined,
6!
2331
          }
2332
        )
2333
      })
2334
    } catch (error) {
2335
      if (isAuthError(error)) {
1✔
2336
        return { data: null, error }
1✔
2337
      }
2338
      throw error
×
2339
    }
2340
  }
2341

2342
  /**
2343
   * Generates a new JWT.
2344
   * @param refreshToken A valid refresh token that was returned on login.
2345
   */
2346
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2347
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
14✔
2348
    this._debug(debugName, 'begin')
14✔
2349

2350
    try {
14✔
2351
      const startedAt = Date.now()
14✔
2352

2353
      // will attempt to refresh the token with exponential backoff
2354
      return await retryable(
14✔
2355
        async (attempt) => {
2356
          if (attempt > 0) {
14!
2357
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2358
          }
2359

2360
          this._debug(debugName, 'refreshing attempt', attempt)
14✔
2361

2362
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
14✔
2363
            body: { refresh_token: refreshToken },
2364
            headers: this.headers,
2365
            xform: _sessionResponse,
2366
          })
2367
        },
2368
        (attempt, error) => {
2369
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
14✔
2370
          return (
14✔
2371
            error &&
19!
2372
            isAuthRetryableFetchError(error) &&
2373
            // retryable only if the request can be sent before the backoff overflows the tick duration
2374
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2375
          )
2376
        }
2377
      )
2378
    } catch (error) {
2379
      this._debug(debugName, 'error', error)
5✔
2380

2381
      if (isAuthError(error)) {
5✔
2382
        return { data: { session: null, user: null }, error }
5✔
2383
      }
2384
      throw error
×
2385
    } finally {
2386
      this._debug(debugName, 'end')
14✔
2387
    }
2388
  }
2389

2390
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2391
    const isValidSession =
2392
      typeof maybeSession === 'object' &&
126✔
2393
      maybeSession !== null &&
2394
      'access_token' in maybeSession &&
2395
      'refresh_token' in maybeSession &&
2396
      'expires_at' in maybeSession
2397

2398
    return isValidSession
126✔
2399
  }
2400

2401
  private async _handleProviderSignIn(
2402
    provider: Provider,
2403
    options: {
2404
      redirectTo?: string
2405
      scopes?: string
2406
      queryParams?: { [key: string]: string }
2407
      skipBrowserRedirect?: boolean
2408
    }
2409
  ) {
2410
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
10✔
2411
      redirectTo: options.redirectTo,
2412
      scopes: options.scopes,
2413
      queryParams: options.queryParams,
2414
    })
2415

2416
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
10✔
2417

2418
    // try to open on the browser
2419
    if (isBrowser() && !options.skipBrowserRedirect) {
10✔
2420
      window.location.assign(url)
3✔
2421
    }
2422

2423
    return { data: { provider, url }, error: null }
10✔
2424
  }
2425

2426
  /**
2427
   * Recovers the session from LocalStorage and refreshes the token
2428
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2429
   */
2430
  private async _recoverAndRefresh() {
2431
    const debugName = '#_recoverAndRefresh()'
33✔
2432
    this._debug(debugName, 'begin')
33✔
2433

2434
    try {
33✔
2435
      const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
33✔
2436

2437
      if (currentSession && this.userStorage) {
33✔
2438
        let maybeUser: { user: User | null } | null = (await getItemAsync(
3✔
2439
          this.userStorage,
2440
          this.storageKey + '-user'
2441
        )) as any
2442

2443
        if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
3!
2444
          // storage and userStorage are the same storage medium, for example
2445
          // window.localStorage if userStorage does not have the user from
2446
          // storage stored, store it first thereby migrating the user object
2447
          // from storage -> userStorage
2448

2449
          maybeUser = { user: currentSession.user }
×
2450
          await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
×
2451
        }
2452

2453
        currentSession.user = maybeUser?.user ?? userNotAvailableProxy()
3✔
2454
      } else if (currentSession && !currentSession.user) {
30✔
2455
        // user storage is not set, let's check if it was previously enabled so
2456
        // we bring back the storage as it should be
2457

2458
        if (!currentSession.user) {
1✔
2459
          // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2460
          const separateUser: { user: User | null } | null = (await getItemAsync(
1✔
2461
            this.storage,
2462
            this.storageKey + '-user'
2463
          )) as any
2464

2465
          if (separateUser && separateUser?.user) {
1!
2466
            currentSession.user = separateUser.user
×
2467

2468
            await removeItemAsync(this.storage, this.storageKey + '-user')
×
2469
            await setItemAsync(this.storage, this.storageKey, currentSession)
×
2470
          } else {
2471
            currentSession.user = userNotAvailableProxy()
1✔
2472
          }
2473
        }
2474
      }
2475

2476
      this._debug(debugName, 'session from storage', currentSession)
32✔
2477

2478
      if (!this._isValidSession(currentSession)) {
32✔
2479
        this._debug(debugName, 'session is not valid')
24✔
2480
        if (currentSession !== null) {
24✔
2481
          await this._removeSession()
3✔
2482
        }
2483

2484
        return
24✔
2485
      }
2486

2487
      const expiresWithMargin =
2488
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
8✔
2489

2490
      this._debug(
8✔
2491
        debugName,
2492
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
8✔
2493
      )
2494

2495
      if (expiresWithMargin) {
8✔
2496
        if (this.autoRefreshToken && currentSession.refresh_token) {
2✔
2497
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
2✔
2498

2499
          if (error) {
2✔
2500
            console.error(error)
1✔
2501

2502
            if (!isAuthRetryableFetchError(error)) {
1✔
2503
              this._debug(
1✔
2504
                debugName,
2505
                'refresh failed with a non-retryable error, removing the session',
2506
                error
2507
              )
2508
              await this._removeSession()
1✔
2509
            }
2510
          }
2511
        }
2512
      } else if (
6✔
2513
        currentSession.user &&
12✔
2514
        (currentSession.user as any).__isUserNotAvailableProxy === true
2515
      ) {
2516
        // If we have a proxy user, try to get the real user data
2517
        try {
3✔
2518
          const { data, error: userError } = await this._getUser(currentSession.access_token)
3✔
2519

2520
          if (!userError && data?.user) {
1!
2521
            currentSession.user = data.user
1✔
2522
            await this._saveSession(currentSession)
1✔
2523
            await this._notifyAllSubscribers('SIGNED_IN', currentSession)
1✔
2524
          } else {
2525
            this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
×
2526
          }
2527
        } catch (getUserError) {
2528
          console.error('Error getting user data:', getUserError)
2✔
2529
          this._debug(
2✔
2530
            debugName,
2531
            'error getting user data, skipping SIGNED_IN notification',
2532
            getUserError
2533
          )
2534
        }
2535
      } else {
2536
        // no need to persist currentSession again, as we just loaded it from
2537
        // local storage; persisting it again may overwrite a value saved by
2538
        // another client with access to the same local storage
2539
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
3✔
2540
      }
2541
    } catch (err) {
2542
      this._debug(debugName, 'error', err)
1✔
2543

2544
      console.error(err)
1✔
2545
      return
1✔
2546
    } finally {
2547
      this._debug(debugName, 'end')
33✔
2548
    }
2549
  }
2550

2551
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2552
    if (!refreshToken) {
19!
2553
      throw new AuthSessionMissingError()
×
2554
    }
2555

2556
    // refreshing is already in progress
2557
    if (this.refreshingDeferred) {
19✔
2558
      return this.refreshingDeferred.promise
3✔
2559
    }
2560

2561
    const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`
16✔
2562

2563
    this._debug(debugName, 'begin')
16✔
2564

2565
    try {
16✔
2566
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
16✔
2567

2568
      const { data, error } = await this._refreshAccessToken(refreshToken)
16✔
2569
      if (error) throw error
15✔
2570
      if (!data.session) throw new AuthSessionMissingError()
9✔
2571

2572
      await this._saveSession(data.session)
8✔
2573
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
8✔
2574

2575
      const result = { data: data.session, error: null }
8✔
2576

2577
      this.refreshingDeferred.resolve(result)
8✔
2578

2579
      return result
8✔
2580
    } catch (error) {
2581
      this._debug(debugName, 'error', error)
8✔
2582

2583
      if (isAuthError(error)) {
8✔
2584
        const result = { data: null, error }
7✔
2585

2586
        if (!isAuthRetryableFetchError(error)) {
7✔
2587
          await this._removeSession()
7✔
2588
        }
2589

2590
        this.refreshingDeferred?.resolve(result)
7!
2591

2592
        return result
7✔
2593
      }
2594

2595
      this.refreshingDeferred?.reject(error)
1!
2596
      throw error
1✔
2597
    } finally {
2598
      this.refreshingDeferred = null
16✔
2599
      this._debug(debugName, 'end')
16✔
2600
    }
2601
  }
2602

2603
  private async _notifyAllSubscribers(
2604
    event: AuthChangeEvent,
2605
    session: Session | null,
2606
    broadcast = true
221✔
2607
  ) {
2608
    const debugName = `#_notifyAllSubscribers(${event})`
222✔
2609
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
222✔
2610

2611
    try {
222✔
2612
      if (this.broadcastChannel && broadcast) {
222!
2613
        this.broadcastChannel.postMessage({ event, session })
×
2614
      }
2615

2616
      const errors: any[] = []
222✔
2617
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
222✔
2618
        try {
5✔
2619
          await x.callback(event, session)
5✔
2620
        } catch (e: any) {
2621
          errors.push(e)
×
2622
        }
2623
      })
2624

2625
      await Promise.all(promises)
222✔
2626

2627
      if (errors.length > 0) {
222!
2628
        for (let i = 0; i < errors.length; i += 1) {
×
2629
          console.error(errors[i])
×
2630
        }
2631

2632
        throw errors[0]
×
2633
      }
2634
    } finally {
2635
      this._debug(debugName, 'end')
222✔
2636
    }
2637
  }
2638

2639
  /**
2640
   * set currentSession and currentUser
2641
   * process to _startAutoRefreshToken if possible
2642
   */
2643
  private async _saveSession(session: Session) {
2644
    this._debug('#_saveSession()', session)
88✔
2645
    // _saveSession is always called whenever a new session has been acquired
2646
    // so we can safely suppress the warning returned by future getSession calls
2647
    this.suppressGetSessionWarning = true
88✔
2648

2649
    // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
2650
    const sessionToProcess = { ...session }
88✔
2651

2652
    const userIsProxy =
2653
      sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
88✔
2654
    if (this.userStorage) {
88!
2655
      if (!userIsProxy && sessionToProcess.user) {
×
2656
        // If it's a real user object, save it to userStorage.
2657
        await setItemAsync(this.userStorage, this.storageKey + '-user', {
×
2658
          user: sessionToProcess.user,
2659
        })
2660
      } else if (userIsProxy) {
×
2661
        // If it's the proxy, it means user was not found in userStorage.
2662
        // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2663
        // or simply not save the proxy. For now, we don't save the proxy here.
2664
        // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2665
      }
2666

2667
      // Prepare the main session data for primary storage: remove the user property before cloning
2668
      // This is important because the original session.user might be the proxy
2669
      const mainSessionData: Omit<Session, 'user'> & { user?: User } = { ...sessionToProcess }
×
2670
      delete mainSessionData.user // Remove user (real or proxy) before cloning for main storage
×
2671

2672
      const clonedMainSessionData = deepClone(mainSessionData)
×
2673
      await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
×
2674
    } else {
2675
      // No userStorage is configured.
2676
      // In this case, session.user should ideally not be a proxy.
2677
      // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2678
      const clonedSession = deepClone(sessionToProcess) // sessionToProcess still has its original user property
88✔
2679
      await setItemAsync(this.storage, this.storageKey, clonedSession)
88✔
2680
    }
2681
  }
2682

2683
  private async _removeSession() {
2684
    this._debug('#_removeSession()')
134✔
2685

2686
    await removeItemAsync(this.storage, this.storageKey)
134✔
2687
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
133✔
2688
    await removeItemAsync(this.storage, this.storageKey + '-user')
133✔
2689

2690
    if (this.userStorage) {
133✔
2691
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
1✔
2692
    }
2693

2694
    await this._notifyAllSubscribers('SIGNED_OUT', null)
133✔
2695
  }
2696

2697
  /**
2698
   * Removes any registered visibilitychange callback.
2699
   *
2700
   * {@see #startAutoRefresh}
2701
   * {@see #stopAutoRefresh}
2702
   */
2703
  private _removeVisibilityChangedCallback() {
2704
    this._debug('#_removeVisibilityChangedCallback()')
17✔
2705

2706
    const callback = this.visibilityChangedCallback
17✔
2707
    this.visibilityChangedCallback = null
17✔
2708

2709
    try {
17✔
2710
      if (callback && isBrowser() && window?.removeEventListener) {
17!
2711
        window.removeEventListener('visibilitychange', callback)
1✔
2712
      }
2713
    } catch (e) {
2714
      console.error('removing visibilitychange callback failed', e)
×
2715
    }
2716
  }
2717

2718
  /**
2719
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2720
   * within the library.
2721
   */
2722
  private async _startAutoRefresh() {
2723
    await this._stopAutoRefresh()
16✔
2724

2725
    this._debug('#_startAutoRefresh()')
16✔
2726

2727
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
16✔
2728
    this.autoRefreshTicker = ticker
16✔
2729

2730
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
16✔
2731
      // ticker is a NodeJS Timeout object that has an `unref` method
2732
      // https://nodejs.org/api/timers.html#timeoutunref
2733
      // When auto refresh is used in NodeJS (like for testing) the
2734
      // `setInterval` is preventing the process from being marked as
2735
      // finished and tests run endlessly. This can be prevented by calling
2736
      // `unref()` on the returned object.
2737
      ticker.unref()
12✔
2738
      // @ts-expect-error TS has no context of Deno
2739
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
4!
2740
      // similar like for NodeJS, but with the Deno API
2741
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2742
      // @ts-expect-error TS has no context of Deno
2743
      Deno.unrefTimer(ticker)
×
2744
    }
2745

2746
    // run the tick immediately, but in the next pass of the event loop so that
2747
    // #_initialize can be allowed to complete without recursively waiting on
2748
    // itself
2749
    setTimeout(async () => {
16✔
2750
      await this.initializePromise
16✔
2751
      await this._autoRefreshTokenTick()
16✔
2752
    }, 0)
2753
  }
2754

2755
  /**
2756
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2757
   * within the library.
2758
   */
2759
  private async _stopAutoRefresh() {
2760
    this._debug('#_stopAutoRefresh()')
20✔
2761

2762
    const ticker = this.autoRefreshTicker
20✔
2763
    this.autoRefreshTicker = null
20✔
2764

2765
    if (ticker) {
20✔
2766
      clearInterval(ticker)
5✔
2767
    }
2768
  }
2769

2770
  /**
2771
   * Starts an auto-refresh process in the background. The session is checked
2772
   * every few seconds. Close to the time of expiration a process is started to
2773
   * refresh the session. If refreshing fails it will be retried for as long as
2774
   * necessary.
2775
   *
2776
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2777
   * to call this function, it will be called for you.
2778
   *
2779
   * On browsers the refresh process works only when the tab/window is in the
2780
   * foreground to conserve resources as well as prevent race conditions and
2781
   * flooding auth with requests. If you call this method any managed
2782
   * visibility change callback will be removed and you must manage visibility
2783
   * changes on your own.
2784
   *
2785
   * On non-browser platforms the refresh process works *continuously* in the
2786
   * background, which may not be desirable. You should hook into your
2787
   * platform's foreground indication mechanism and call these methods
2788
   * appropriately to conserve resources.
2789
   *
2790
   * {@see #stopAutoRefresh}
2791
   */
2792
  async startAutoRefresh() {
2793
    this._removeVisibilityChangedCallback()
13✔
2794
    await this._startAutoRefresh()
13✔
2795
  }
2796

2797
  /**
2798
   * Stops an active auto refresh process running in the background (if any).
2799
   *
2800
   * If you call this method any managed visibility change callback will be
2801
   * removed and you must manage visibility changes on your own.
2802
   *
2803
   * See {@link #startAutoRefresh} for more details.
2804
   */
2805
  async stopAutoRefresh() {
2806
    this._removeVisibilityChangedCallback()
4✔
2807
    await this._stopAutoRefresh()
4✔
2808
  }
2809

2810
  /**
2811
   * Runs the auto refresh token tick.
2812
   */
2813
  private async _autoRefreshTokenTick() {
2814
    this._debug('#_autoRefreshTokenTick()', 'begin')
16✔
2815

2816
    try {
16✔
2817
      await this._acquireLock(0, async () => {
16✔
2818
        try {
16✔
2819
          const now = Date.now()
16✔
2820

2821
          try {
16✔
2822
            return await this._useSession(async (result) => {
16✔
2823
              const {
2824
                data: { session },
2825
              } = result
16✔
2826

2827
              if (!session || !session.refresh_token || !session.expires_at) {
16✔
2828
                this._debug('#_autoRefreshTokenTick()', 'no session')
10✔
2829
                return
10✔
2830
              }
2831

2832
              // session will expire in this many ticks (or has already expired if <= 0)
2833
              const expiresInTicks = Math.floor(
6✔
2834
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2835
              )
2836

2837
              this._debug(
6✔
2838
                '#_autoRefreshTokenTick()',
2839
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2840
              )
2841

2842
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
6!
2843
                await this._callRefreshToken(session.refresh_token)
×
2844
              }
2845
            })
2846
          } catch (e: any) {
2847
            console.error(
×
2848
              'Auto refresh tick failed with error. This is likely a transient error.',
2849
              e
2850
            )
2851
          }
2852
        } finally {
2853
          this._debug('#_autoRefreshTokenTick()', 'end')
16✔
2854
        }
2855
      })
2856
    } catch (e: any) {
2857
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2858
        this._debug('auto refresh token tick lock not available')
×
2859
      } else {
2860
        throw e
×
2861
      }
2862
    }
2863
  }
2864

2865
  /**
2866
   * Registers callbacks on the browser / platform, which in-turn run
2867
   * algorithms when the browser window/tab are in foreground. On non-browser
2868
   * platforms it assumes always foreground.
2869
   */
2870
  private async _handleVisibilityChange() {
2871
    this._debug('#_handleVisibilityChange()')
88✔
2872

2873
    if (!isBrowser() || !window?.addEventListener) {
88!
2874
      if (this.autoRefreshToken) {
53✔
2875
        // in non-browser environments the refresh token ticker runs always
2876
        this.startAutoRefresh()
10✔
2877
      }
2878

2879
      return false
53✔
2880
    }
2881

2882
    try {
35✔
2883
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
35✔
2884

2885
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
35!
2886

2887
      // now immediately call the visbility changed callback to setup with the
2888
      // current visbility state
2889
      await this._onVisibilityChanged(true) // initial call
34✔
2890
    } catch (error) {
2891
      console.error('_handleVisibilityChange', error)
1✔
2892
    }
2893
  }
2894

2895
  /**
2896
   * Callback registered with `window.addEventListener('visibilitychange')`.
2897
   */
2898
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2899
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
34✔
2900
    this._debug(methodName, 'visibilityState', document.visibilityState)
34✔
2901

2902
    if (document.visibilityState === 'visible') {
34!
2903
      if (this.autoRefreshToken) {
34✔
2904
        // in browser environments the refresh token ticker runs only on focused tabs
2905
        // which prevents race conditions
2906
        this._startAutoRefresh()
3✔
2907
      }
2908

2909
      if (!calledFromInitialize) {
34!
2910
        // called when the visibility has changed, i.e. the browser
2911
        // transitioned from hidden -> visible so we need to see if the session
2912
        // should be recovered immediately... but to do that we need to acquire
2913
        // the lock first asynchronously
2914
        await this.initializePromise
×
2915

2916
        await this._acquireLock(-1, async () => {
×
2917
          if (document.visibilityState !== 'visible') {
×
2918
            this._debug(
×
2919
              methodName,
2920
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2921
            )
2922

2923
            // visibility has changed while waiting for the lock, abort
2924
            return
×
2925
          }
2926

2927
          // recover the session
2928
          await this._recoverAndRefresh()
×
2929
        })
2930
      }
2931
    } else if (document.visibilityState === 'hidden') {
×
2932
      if (this.autoRefreshToken) {
×
2933
        this._stopAutoRefresh()
×
2934
      }
2935
    }
2936
  }
2937

2938
  /**
2939
   * Generates the relevant login URL for a third-party provider.
2940
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2941
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2942
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2943
   */
2944
  private async _getUrlForProvider(
2945
    url: string,
2946
    provider: Provider,
2947
    options: {
2948
      redirectTo?: string
2949
      scopes?: string
2950
      queryParams?: { [key: string]: string }
2951
      skipBrowserRedirect?: boolean
2952
    }
2953
  ) {
2954
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
14✔
2955
    if (options?.redirectTo) {
14!
2956
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
7✔
2957
    }
2958
    if (options?.scopes) {
14!
2959
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
3✔
2960
    }
2961
    if (this.flowType === 'pkce') {
14✔
2962
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
2963
        this.storage,
2964
        this.storageKey
2965
      )
2966

2967
      const flowParams = new URLSearchParams({
2✔
2968
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2969
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2970
      })
2971
      urlParams.push(flowParams.toString())
2✔
2972
    }
2973
    if (options?.queryParams) {
14!
2974
      const query = new URLSearchParams(options.queryParams)
2✔
2975
      urlParams.push(query.toString())
2✔
2976
    }
2977
    if (options?.skipBrowserRedirect) {
14!
2978
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
2✔
2979
    }
2980

2981
    return `${url}?${urlParams.join('&')}`
14✔
2982
  }
2983

2984
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2985
    try {
2✔
2986
      return await this._useSession(async (result) => {
2✔
2987
        const { data: sessionData, error: sessionError } = result
2✔
2988
        if (sessionError) {
2!
2989
          return { data: null, error: sessionError }
×
2990
        }
2991

2992
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
2✔
2993
          headers: this.headers,
2994
          jwt: sessionData?.session?.access_token,
11!
2995
        })
2996
      })
2997
    } catch (error) {
2998
      if (isAuthError(error)) {
1✔
2999
        return { data: null, error }
1✔
3000
      }
3001
      throw error
×
3002
    }
3003
  }
3004

3005
  /**
3006
   * {@see GoTrueMFAApi#enroll}
3007
   */
3008
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
3009
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
3010
  private async _enroll(params: MFAEnrollWebauthnParams): Promise<AuthMFAEnrollWebauthnResponse>
3011
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
3012
    try {
10✔
3013
      return await this._useSession(async (result) => {
10✔
3014
        const { data: sessionData, error: sessionError } = result
10✔
3015
        if (sessionError) {
10!
3016
          return { data: null, error: sessionError }
×
3017
        }
3018

3019
        const body = {
10✔
3020
          friendly_name: params.friendlyName,
3021
          factor_type: params.factorType,
3022
          ...(params.factorType === 'phone'
10✔
3023
            ? { phone: params.phone }
3024
            : params.factorType === 'totp'
9!
3025
            ? { issuer: params.issuer }
3026
            : {}),
3027
        }
3028

3029
        const { data, error } = (await _request(this.fetch, 'POST', `${this.url}/factors`, {
10✔
3030
          body,
3031
          headers: this.headers,
3032
          jwt: sessionData?.session?.access_token,
58!
3033
        })) as AuthMFAEnrollResponse
3034
        if (error) {
8!
3035
          return { data: null, error }
×
3036
        }
3037

3038
        if (params.factorType === 'totp' && data.type === 'totp' && data?.totp?.qr_code) {
8!
3039
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
7✔
3040
        }
3041

3042
        return { data, error: null }
8✔
3043
      })
3044
    } catch (error) {
3045
      if (isAuthError(error)) {
2✔
3046
        return { data: null, error }
2✔
3047
      }
3048
      throw error
×
3049
    }
3050
  }
3051

3052
  /**
3053
   * {@see GoTrueMFAApi#verify}
3054
   */
3055
  private async _verify(params: MFAVerifyTOTPParams): Promise<AuthMFAVerifyResponse>
3056
  private async _verify(params: MFAVerifyPhoneParams): Promise<AuthMFAVerifyResponse>
3057
  private async _verify<T extends 'create' | 'request'>(
3058
    params: MFAVerifyWebauthnParams<T>
3059
  ): Promise<AuthMFAVerifyResponse>
3060
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
3061
    return this._acquireLock(-1, async () => {
3✔
3062
      try {
3✔
3063
        return await this._useSession(async (result) => {
3✔
3064
          const { data: sessionData, error: sessionError } = result
3✔
3065
          if (sessionError) {
3!
3066
            return { data: null, error: sessionError }
×
3067
          }
3068

3069
          const body: StrictOmit<
3✔
3070
            | Exclude<MFAVerifyParams, MFAVerifyWebauthnParams>
3071
            /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */
3072
            | Prettify<
3073
                StrictOmit<MFAVerifyWebauthnParams, 'webauthn'> & {
3074
                  webauthn: Prettify<
3075
                    | StrictOmit<
3076
                        MFAVerifyWebauthnParamFields['webauthn'],
3077
                        'credential_response'
3078
                      > & {
3079
                        credential_response: PublicKeyCredentialJSON
3080
                      }
3081
                  >
3082
                }
3083
              >,
3084
            /*  Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */
3085
            'challengeId' | 'factorId'
3086
          > & {
3087
            challenge_id: string
3088
          } = {
3089
            challenge_id: params.challengeId,
3090
            ...('webauthn' in params
3!
3091
              ? {
3092
                  webauthn: {
3093
                    ...params.webauthn,
3094
                    credential_response:
3095
                      params.webauthn.type === 'create'
×
3096
                        ? serializeCredentialCreationResponse(
3097
                            params.webauthn.credential_response as RegistrationCredential
3098
                          )
3099
                        : serializeCredentialRequestResponse(
3100
                            params.webauthn.credential_response as AuthenticationCredential
3101
                          ),
3102
                  },
3103
                }
3104
              : { code: params.code }),
3105
          }
3106

3107
          const { data, error } = await _request(
3✔
3108
            this.fetch,
3109
            'POST',
3110
            `${this.url}/factors/${params.factorId}/verify`,
3111
            {
3112
              body,
3113
              headers: this.headers,
3114
              jwt: sessionData?.session?.access_token,
17!
3115
            }
3116
          )
3117
          if (error) {
×
3118
            return { data: null, error }
×
3119
          }
3120

3121
          await this._saveSession({
×
3122
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
3123
            ...data,
3124
          })
3125
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
3126

3127
          return { data, error }
×
3128
        })
3129
      } catch (error) {
3130
        if (isAuthError(error)) {
3✔
3131
          return { data: null, error }
3✔
3132
        }
3133
        throw error
×
3134
      }
3135
    })
3136
  }
3137

3138
  /**
3139
   * {@see GoTrueMFAApi#challenge}
3140
   */
3141
  private async _challenge(
3142
    params: MFAChallengeTOTPParams
3143
  ): Promise<Prettify<AuthMFAChallengeTOTPResponse>>
3144
  private async _challenge(
3145
    params: MFAChallengePhoneParams
3146
  ): Promise<Prettify<AuthMFAChallengePhoneResponse>>
3147
  private async _challenge(
3148
    params: MFAChallengeWebauthnParams
3149
  ): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
3150
  private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
3151
    return this._acquireLock(-1, async () => {
4✔
3152
      try {
4✔
3153
        return await this._useSession(async (result) => {
4✔
3154
          const { data: sessionData, error: sessionError } = result
4✔
3155
          if (sessionError) {
4!
3156
            return { data: null, error: sessionError }
×
3157
          }
3158

3159
          const response = (await _request(
4✔
3160
            this.fetch,
3161
            'POST',
3162
            `${this.url}/factors/${params.factorId}/challenge`,
3163
            {
3164
              body: params,
3165
              headers: this.headers,
3166
              jwt: sessionData?.session?.access_token,
23!
3167
            }
3168
          )) as
3169
            | Exclude<AuthMFAChallengeResponse, AuthMFAChallengeWebauthnResponse>
3170
            /** The server will send `serialized` data, so we assert the serialized response */
3171
            | AuthMFAChallengeWebauthnServerResponse
3172

3173
          if (response.error) {
3!
NEW
3174
            return response
×
3175
          }
3176

3177
          const { data } = response
3✔
3178

3179
          if (data.type !== 'webauthn') {
3✔
3180
            return { data, error: null }
3✔
3181
          }
3182

NEW
3183
          switch (data.webauthn.type) {
×
3184
            case 'create':
NEW
3185
              return {
×
3186
                data: {
3187
                  ...data,
3188
                  webauthn: {
3189
                    ...data.webauthn,
3190
                    credential_options: {
3191
                      ...data.webauthn.credential_options,
3192
                      publicKey: deserializeCredentialCreationOptions(
3193
                        data.webauthn.credential_options.publicKey
3194
                      ),
3195
                    },
3196
                  },
3197
                },
3198
                error: null,
3199
              }
3200
            case 'request':
NEW
3201
              return {
×
3202
                data: {
3203
                  ...data,
3204
                  webauthn: {
3205
                    ...data.webauthn,
3206
                    credential_options: {
3207
                      ...data.webauthn.credential_options,
3208
                      publicKey: deserializeCredentialRequestOptions(
3209
                        data.webauthn.credential_options.publicKey
3210
                      ),
3211
                    },
3212
                  },
3213
                },
3214
                error: null,
3215
              }
3216
          }
3217
        })
3218
      } catch (error) {
3219
        if (isAuthError(error)) {
1✔
3220
          return { data: null, error }
1✔
3221
        }
3222
        throw error
×
3223
      }
3224
    })
3225
  }
3226

3227
  /**
3228
   * {@see GoTrueMFAApi#challengeAndVerify}
3229
   */
3230
  private async _challengeAndVerify(
3231
    params: MFAChallengeAndVerifyParams
3232
  ): Promise<AuthMFAVerifyResponse> {
3233
    // both _challenge and _verify independently acquire the lock, so no need
3234
    // to acquire it here
3235

3236
    const { data: challengeData, error: challengeError } = await this._challenge({
1✔
3237
      factorId: params.factorId,
3238
    })
3239
    if (challengeError) {
1!
3240
      return { data: null, error: challengeError }
×
3241
    }
3242

3243
    return await this._verify({
1✔
3244
      factorId: params.factorId,
3245
      challengeId: challengeData.id,
3246
      code: params.code,
3247
    })
3248
  }
3249

3250
  /**
3251
   * {@see GoTrueMFAApi#listFactors}
3252
   */
3253
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
3254
    // use #getUser instead of #_getUser as the former acquires a lock
3255
    const {
3256
      data: { user },
3257
      error: userError,
3258
    } = await this.getUser()
2✔
3259
    if (userError) {
2!
3260
      return { data: null, error: userError }
×
3261
    }
3262

3263
    const data: AuthMFAListFactorsResponse['data'] = {
2✔
3264
      all: [],
3265
      phone: [],
3266
      totp: [],
3267
      webauthn: [],
3268
    }
3269

3270
    // loop over the factors ONCE
3271
    for (const factor of user?.factors ?? []) {
2!
3272
      data.all.push(factor)
4✔
3273
      if (factor.status === 'verified') {
4✔
3274
        ;(data[factor.factor_type] as typeof factor[]).push(factor)
2✔
3275
      }
3276
    }
3277

3278
    return {
2✔
3279
      data,
3280
      error: null,
3281
    }
3282
  }
3283

3284
  /**
3285
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
3286
   */
3287
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
3288
    return this._acquireLock(-1, async () => {
1✔
3289
      return await this._useSession(async (result) => {
1✔
3290
        const {
3291
          data: { session },
3292
          error: sessionError,
3293
        } = result
1✔
3294
        if (sessionError) {
1!
3295
          return { data: null, error: sessionError }
×
3296
        }
3297
        if (!session) {
1!
3298
          return {
×
3299
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
3300
            error: null,
3301
          }
3302
        }
3303

3304
        const { payload } = decodeJWT(session.access_token)
1✔
3305

3306
        let currentLevel: AuthenticatorAssuranceLevels | null = null
1✔
3307

3308
        if (payload.aal) {
1✔
3309
          currentLevel = payload.aal
1✔
3310
        }
3311

3312
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
1✔
3313

3314
        const verifiedFactors =
3315
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
1!
3316

3317
        if (verifiedFactors.length > 0) {
1!
3318
          nextLevel = 'aal2'
×
3319
        }
3320

3321
        const currentAuthenticationMethods = payload.amr || []
1!
3322

3323
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
1✔
3324
      })
3325
    })
3326
  }
3327

3328
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
4✔
3329
    // try fetching from the supplied jwks
3330
    let jwk = jwks.keys.find((key) => key.kid === kid)
4✔
3331
    if (jwk) {
4!
3332
      return jwk
×
3333
    }
3334

3335
    const now = Date.now()
4✔
3336

3337
    // try fetching from cache
3338
    jwk = this.jwks.keys.find((key) => key.kid === kid)
4✔
3339

3340
    // jwk exists and jwks isn't stale
3341
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
4✔
3342
      return jwk
1✔
3343
    }
3344
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
3345
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
3✔
3346
      headers: this.headers,
3347
    })
3348
    if (error) {
3!
3349
      throw error
×
3350
    }
3351
    if (!data.keys || data.keys.length === 0) {
3!
3352
      return null
×
3353
    }
3354

3355
    this.jwks = data
3✔
3356
    this.jwks_cached_at = now
3✔
3357

3358
    // Find the signing key
3359
    jwk = data.keys.find((key: any) => key.kid === kid)
3✔
3360
    if (!jwk) {
3!
3361
      return null
×
3362
    }
3363
    return jwk
3✔
3364
  }
3365

3366
  /**
3367
   * Extracts the JWT claims present in the access token by first verifying the
3368
   * JWT against the server's JSON Web Key Set endpoint
3369
   * `/.well-known/jwks.json` which is often cached, resulting in significantly
3370
   * faster responses. Prefer this method over {@link #getUser} which always
3371
   * sends a request to the Auth server for each JWT.
3372
   *
3373
   * If the project is not using an asymmetric JWT signing key (like ECC or
3374
   * RSA) it always sends a request to the Auth server (similar to {@link
3375
   * #getUser}) to verify the JWT.
3376
   *
3377
   * @param jwt An optional specific JWT you wish to verify, not the one you
3378
   *            can obtain from {@link #getSession}.
3379
   * @param options Various additional options that allow you to customize the
3380
   *                behavior of this method.
3381
   */
3382
  async getClaims(
3383
    jwt?: string,
3384
    options: {
7✔
3385
      /**
3386
       * @deprecated Please use options.jwks instead.
3387
       */
3388
      keys?: JWK[]
3389

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

3393
      /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3394
      jwks?: { keys: JWK[] }
3395
    } = {}
3396
  ): Promise<
3397
    | {
3398
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3399
        error: null
3400
      }
3401
    | { data: null; error: AuthError }
3402
    | { data: null; error: null }
3403
  > {
3404
    try {
7✔
3405
      let token = jwt
7✔
3406
      if (!token) {
7✔
3407
        const { data, error } = await this.getSession()
7✔
3408
        if (error || !data.session) {
7✔
3409
          return { data: null, error }
1✔
3410
        }
3411
        token = data.session.access_token
6✔
3412
      }
3413

3414
      const {
3415
        header,
3416
        payload,
3417
        signature,
3418
        raw: { header: rawHeader, payload: rawPayload },
3419
      } = decodeJWT(token)
6✔
3420

3421
      if (!options?.allowExpired) {
5!
3422
        // Reject expired JWTs should only happen if jwt argument was passed
3423
        validateExp(payload.exp)
5✔
3424
      }
3425

3426
      const signingKey =
3427
        !header.alg ||
4!
3428
        header.alg.startsWith('HS') ||
3429
        !header.kid ||
3430
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
1!
3431
          ? null
3432
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
×
3433

3434
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3435
      if (!signingKey) {
4✔
3436
        const { error } = await this.getUser(token)
4✔
3437
        if (error) {
4✔
3438
          throw error
1✔
3439
        }
3440
        // getUser succeeds so the claims in the JWT can be trusted
3441
        return {
3✔
3442
          data: {
3443
            claims: payload,
3444
            header,
3445
            signature,
3446
          },
3447
          error: null,
3448
        }
3449
      }
3450

UNCOV
3451
      const algorithm = getAlgorithm(header.alg)
×
3452

3453
      // Convert JWK to CryptoKey
UNCOV
3454
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
×
3455
        'verify',
3456
      ])
3457

3458
      // Verify the signature
UNCOV
3459
      const isValid = await crypto.subtle.verify(
×
3460
        algorithm,
3461
        publicKey,
3462
        signature,
3463
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3464
      )
3465

UNCOV
3466
      if (!isValid) {
×
UNCOV
3467
        throw new AuthInvalidJwtError('Invalid JWT signature')
×
3468
      }
3469

3470
      // If verification succeeds, decode and return claims
UNCOV
3471
      return {
×
3472
        data: {
3473
          claims: payload,
3474
          header,
3475
          signature,
3476
        },
3477
        error: null,
3478
      }
3479
    } catch (error) {
3480
      if (isAuthError(error)) {
3✔
3481
        return { data: null, error }
2✔
3482
      }
3483
      throw error
1✔
3484
    }
3485
  }
3486
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc