import { inBrowser } from '@clerk/shared/browser'; import { type ClerkError, ClerkRuntimeError, ClerkWebAuthnError } from '@clerk/shared/error'; import { ERROR_CODES } from '@clerk/shared/internal/clerk-js/constants'; import { convertJSONToPublicKeyRequestOptions, serializePublicKeyCredentialAssertion, webAuthnGetCredential as webAuthnGetCredentialOnWindow, } from '@clerk/shared/internal/clerk-js/passkeys'; import { createValidatePassword } from '@clerk/shared/internal/clerk-js/passwords/password'; import { getClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams'; import { Poller } from '@clerk/shared/poller'; import type { AttemptFirstFactorParams, AttemptSecondFactorParams, AuthenticateWithPasskeyParams, AuthenticateWithPopupParams, AuthenticateWithRedirectParams, AuthenticateWithWeb3Params, CaptchaWidgetType, ClientTrustState, CreateEmailLinkFlowReturn, EmailCodeConfig, EmailCodeFactor, EmailLinkConfig, EmailLinkFactor, EnterpriseSSOConfig, GenerateSignature, PassKeyConfig, PasskeyFactor, PhoneCodeConfig, PhoneCodeFactor, PrepareFirstFactorParams, PrepareSecondFactorParams, ProtectCheckResource, ResetPasswordEmailCodeFactorConfig, ResetPasswordParams, ResetPasswordPhoneCodeFactorConfig, SignInAuthenticateWithSolanaParams, SignInCreateParams, SignInFirstFactor, SignInFutureBackupCodeVerifyParams, SignInFutureCreateParams, SignInFutureEmailCodeSendParams, SignInFutureEmailCodeVerifyParams, SignInFutureEmailLinkSendParams, SignInFutureFinalizeParams, SignInFutureMFAEmailCodeVerifyParams, SignInFutureMFAPhoneCodeVerifyParams, SignInFuturePasskeyParams, SignInFuturePasswordParams, SignInFuturePhoneCodeSendParams, SignInFuturePhoneCodeVerifyParams, SignInFutureResetPasswordPhoneCodeSendParams, SignInFutureResetPasswordPhoneCodeVerifyParams, SignInFutureResetPasswordSubmitParams, SignInFutureResource, SignInFutureSSOParams, SignInFutureSubmitProtectCheckParams, SignInFutureTicketParams, SignInFutureTOTPVerifyParams, SignInFutureWeb3Params, SignInIdentifier, SignInJSON, SignInJSONSnapshot, SignInResource, SignInSecondFactor, SignInStartEmailLinkFlowParams, SignInStatus, VerificationResource, Web3Provider, Web3SignatureConfig, Web3SignatureFactor, } from '@clerk/shared/types'; import { deepCamelToSnake, deepSnakeToCamel } from '@clerk/shared/underscore'; import { isWebAuthnAutofillSupported as isWebAuthnAutofillSupportedOnWindow, isWebAuthnSupported as isWebAuthnSupportedOnWindow, } from '@clerk/shared/webauthn'; import { debugLogger } from '@/utils/debug'; import { getBrowserLocale, web3 } from '../../utils'; import { _authenticateWithPopup, _futureAuthenticateWithPopup, wrapWithPopupRoutes, } from '../../utils/authenticateWithPopup'; import { _authenticateWithTransport } from '../../utils/authenticateWithTransport'; import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge'; import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask'; import { getBrowserTimezone } from '../../utils/timezone'; import { loadZxcvbn } from '../../utils/zxcvbn'; import { clerkInvalidFAPIResponse, clerkInvalidStrategy, clerkMissingOptionError, clerkMissingWebAuthnPublicKeyOptions, clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyPasskeyCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; import { eventBus } from '../events'; import { ProtectCheckGate } from '../protectCheckGate'; import { type BaseMutateParams, BaseResource, UserData, Verification } from './internal'; /** * Terminal states for email-link verification polling: `verified` (success), `expired` * (link timed out), or `transferable` (`signUpIfMissing` flows — the address was verified * but no user exists, so the caller transfers to sign-up). Shared by the legacy * `createEmailLinkFlow` poll and `SignInFuture.waitForEmailLinkVerification` so the two * loops can't drift apart. */ const isTerminalEmailLinkVerificationStatus = (status: string | null) => status === 'verified' || status === 'expired' || status === 'transferable'; export class SignIn extends BaseResource implements SignInResource { pathRoot = '/client/sign_ins'; id?: string; private _status: SignInStatus | null = null; supportedIdentifiers: SignInIdentifier[] = []; supportedFirstFactors: SignInFirstFactor[] | null = []; supportedSecondFactors: SignInSecondFactor[] | null = null; ssoBypassFirstFactors: SignInFirstFactor[] | null = null; firstFactorVerification: VerificationResource = new Verification(null); secondFactorVerification: VerificationResource = new Verification(null); identifier: string | null = null; createdSessionId: string | null = null; userData: UserData = new UserData(null); clientTrustState?: ClientTrustState; protectCheck: ProtectCheckResource | null = null; timezone: string | null = null; /** * The current status of the sign-in process. * * @returns The current sign-in status, or null if no status has been set */ get status(): SignInStatus | null { return this._status; } /** * Sets the sign-in status and logs the transition at debug level. * * @param value - The new status to set. Can be null to clear the status. * @remarks When setting a new status that differs from the previous one, * a debug log entry is created showing the transition from the old to new status. */ set status(value: SignInStatus | null) { const previousStatus = this._status; this._status = value; if (value && previousStatus !== value) { debugLogger.debug('SignIn.status', { id: this.id, from: previousStatus, to: value }); } } /** * @experimental This experimental API is subject to change. * * An instance of `SignInFuture`, which has a different API than `SignIn`, intended to be used in custom flows. */ __internal_future: SignInFuture = new SignInFuture(this); /** * @internal Only used for internal purposes, and is not intended to be used directly. * * This property is used to provide access to underlying Client methods to `SignInFuture`, which wraps an instance * of `SignIn`. */ __internal_basePost = async ( params?: BaseMutateParams, { resolveProtectCheck = true }: { resolveProtectCheck?: boolean } = {}, ): Promise => { await this._basePost(params); if (resolveProtectCheck) { await ProtectCheckGate.getInstance().resolve(SignIn.clerk, this, params?.action); } return this; }; /** * @internal Only used for internal purposes, and is not intended to be used directly. * * This property is used to provide access to underlying Client methods to `SignInFuture`, which wraps an instance * of `SignIn`. */ __internal_basePatch = async ( params?: BaseMutateParams, { resolveProtectCheck = true }: { resolveProtectCheck?: boolean } = {}, ): Promise => { await this._basePatch(params); if (resolveProtectCheck) { await ProtectCheckGate.getInstance().resolve(SignIn.clerk, this, params?.action); } return this; }; /** * @internal Only used for internal purposes, and is not intended to be used directly. * * This property is used to provide access to underlying Client methods to `SignInFuture`, which wraps an instance * of `SignIn`. */ __internal_baseGet = this._baseGet.bind(this); constructor(data: SignInJSON | SignInJSONSnapshot | null = null) { super(); this.fromJSON(data); } create = async (params: SignInCreateParams): Promise => { debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined }); let body: Record = { ...params }; // Inject browser locale const browserLocale = getBrowserLocale(); if (browserLocale) { body.locale = browserLocale; } if (body.timezone === undefined) { const browserTimezone = getBrowserTimezone(); if (browserTimezone) { body.timezone = browserTimezone; } } if ( this.shouldRequireCaptcha(params) && !__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params) ) { const captchaChallenge = new CaptchaChallenge(SignIn.clerk); const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signin' }); if (!captchaParams) { throw new ClerkRuntimeError('', { code: 'captcha_unavailable' }); } body = { ...body, ...captchaParams }; } return this._basePost({ path: this.pathRoot, body: body, }); }; resetPassword = (params: ResetPasswordParams): Promise => { return this._basePost({ body: params, action: 'reset_password', }); }; prepareFirstFactor = (params: PrepareFirstFactorParams): Promise => { debugLogger.debug('SignIn.prepareFirstFactor', { id: this.id, strategy: params.strategy }); let config; switch (params.strategy) { case 'passkey': config = {} as PassKeyConfig; break; case 'email_link': config = { emailAddressId: params.emailAddressId, redirectUrl: params.redirectUrl, } as EmailLinkConfig; break; case 'email_code': config = { emailAddressId: params.emailAddressId } as EmailCodeConfig; break; case 'phone_code': config = { phoneNumberId: params.phoneNumberId, default: params.default, channel: params.channel, } as PhoneCodeConfig; break; case 'web3_metamask_signature': case 'web3_base_signature': case 'web3_coinbase_wallet_signature': case 'web3_okx_wallet_signature': case 'web3_solana_signature': config = { web3WalletId: params.web3WalletId } as Web3SignatureConfig; break; case 'reset_password_phone_code': config = { phoneNumberId: params.phoneNumberId } as ResetPasswordPhoneCodeFactorConfig; break; case 'reset_password_email_code': config = { emailAddressId: params.emailAddressId } as ResetPasswordEmailCodeFactorConfig; break; case 'enterprise_sso': config = { redirectUrl: params.redirectUrl, actionCompleteRedirectUrl: params.actionCompleteRedirectUrl, oidcPrompt: params.oidcPrompt, enterpriseConnectionId: params.enterpriseConnectionId, } as EnterpriseSSOConfig; break; default: clerkInvalidStrategy('SignIn.prepareFirstFactor', params.strategy); } return this._basePost({ body: { ...config, strategy: params.strategy }, action: 'prepare_first_factor', coalesce: true, }); }; /** * Submits a proof token to resolve a Clerk Protect challenge (`protect_check`) during sign-in. * * @param params - The proof token parameters. * @param params.proofToken - The proof token produced by the Protect challenge SDK. * @returns A promise resolving to the updated `SignIn` resource (gate cleared, a chained * challenge, or the completed flow). */ submitProtectCheck = (params: { proofToken: string }): Promise => { debugLogger.debug('SignIn.submitProtectCheck', { id: this.id }); return this._basePatch({ action: 'protect_check', body: { proof_token: params.proofToken }, }); }; attemptFirstFactor = (params: AttemptFirstFactorParams): Promise => { debugLogger.debug('SignIn.attemptFirstFactor', { id: this.id, strategy: params.strategy }); let config; switch (params.strategy) { case 'passkey': config = { publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(params.publicKeyCredential)), }; break; default: config = { ...params }; } return this._basePost({ body: { ...config, strategy: params.strategy }, action: 'attempt_first_factor', }); }; createEmailLinkFlow = (): CreateEmailLinkFlowReturn => { const { run, stop } = Poller(); const startEmailLinkFlow = async ({ emailAddressId, redirectUrl, }: SignInStartEmailLinkFlowParams): Promise => { if (!this.id) { clerkVerifyEmailAddressCalledBeforeCreate('SignIn'); } const emailLinkParams: EmailLinkConfig = { strategy: 'email_link', emailAddressId, redirectUrl, }; const isSecondFactor = this.status === 'needs_second_factor' || this.status === 'needs_client_trust'; const verificationKey: 'firstFactorVerification' | 'secondFactorVerification' = isSecondFactor ? 'secondFactorVerification' : 'firstFactorVerification'; if (isSecondFactor) { await this.prepareSecondFactor(emailLinkParams); } else { await this.prepareFirstFactor(emailLinkParams); } return new Promise((resolve, reject) => { void run(() => { return this.reload() .then(res => { if (isTerminalEmailLinkVerificationStatus(res[verificationKey].status)) { stop(); resolve(res); } }) .catch(err => { stop(); reject(err); }); }); }); }; return { startEmailLinkFlow, cancelEmailLinkFlow: stop }; }; prepareSecondFactor = (params: PrepareSecondFactorParams): Promise => { debugLogger.debug('SignIn.prepareSecondFactor', { id: this.id, strategy: params.strategy }); return this._basePost({ body: params, action: 'prepare_second_factor', coalesce: true, }); }; attemptSecondFactor = (params: AttemptSecondFactorParams): Promise => { debugLogger.debug('SignIn.attemptSecondFactor', { id: this.id, strategy: params.strategy }); let config; switch (params.strategy) { case 'passkey': config = { publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(params.publicKeyCredential)), }; break; default: config = { ...params }; } return this._basePost({ body: { ...config, strategy: params.strategy }, action: 'attempt_second_factor', }); }; private authenticateWithRedirectOrPopup = async ( params: AuthenticateWithRedirectParams, navigateCallback: (url: URL | string) => void, ): Promise => { const { strategy, redirectUrlComplete, identifier, oidcPrompt, slackTeamIdHint, continueSignIn, enterpriseConnectionId, } = params || {}; const actionCompleteRedirectUrl = redirectUrlComplete; const redirectUrl = SignIn.clerk.buildUrlWithAuth(params.redirectUrl); const isChallengePending = () => !!this.protectCheck || this.status === 'needs_protect_check'; const pendingHandOff = () => { const { status, externalVerificationRedirectURL } = this.firstFactorVerification; return status === 'unverified' ? externalVerificationRedirectURL : null; }; // A pending challenge with nowhere to navigate to. Throw rather than return, so the method still // either navigates or throws: a caller that doesn't handle challenges gets an error it can // recognise instead of a silent success. A caller that does runs the challenge and calls back // in with `continueSignIn`. const throwChallengeRequired = (): never => { throw new ClerkRuntimeError('A verification challenge must be completed before this sign-in can continue.', { code: ERROR_CODES.PROTECT_CHECK_REQUIRED, }); }; // The hand-off a challenged create built, if any. The server builds it before deciding, so a // challenge on create can arrive with a usable redirect: that means "go to the identity // provider first" and the challenge runs on the way back, where the callback routes to it. let challengedCreateHandOff: URL | null = null; const shouldCreateSignIn = !this.id || !continueSignIn; if (shouldCreateSignIn) { await this.create({ strategy, identifier, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, slackTeamIdHint, }); if (isChallengePending()) { challengedCreateHandOff = pendingHandOff(); if (!challengedCreateHandOff) { throwChallengeRequired(); } } } if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamIdHint !== undefined)) { const prepareParams = { strategy, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, slackTeamIdHint, enterpriseConnectionId, }; await this._basePost({ body: prepareParams, action: 'prepare_first_factor', coalesce: true }); // A challenged prepare builds no verification, so any redirect left on the sign-in is from an // earlier attempt and may be for another connection. Only this call's create hand-off is safe // to follow. if (isChallengePending()) { if (challengedCreateHandOff) { navigateCallback(challengedCreateHandOff); return; } throwChallengeRequired(); } } const { status, externalVerificationRedirectURL } = this.firstFactorVerification; if (status === 'unverified' && externalVerificationRedirectURL) { navigateCallback(externalVerificationRedirectURL); } else { clerkInvalidFAPIResponse(status, SignIn.fapiClient.buildEmailAddress('support')); } }; public authenticateWithRedirect = async (params: AuthenticateWithRedirectParams): Promise => { const transport = SignIn.clerk.__internal_oauthTransport; if (transport) { return _authenticateWithTransport({ clerk: SignIn.clerk, transport, resource: this, authenticateMethod: this.authenticateWithRedirectOrPopup, params, callbackParams: params.__internal_callbackParams ?? {}, }); } return this.authenticateWithRedirectOrPopup(params, SignIn.clerk.__internal_windowNavigate); }; public authenticateWithPopup = async (params: AuthenticateWithPopupParams): Promise => { const { popup } = params || {}; if (!popup) { clerkMissingOptionError('popup'); } return _authenticateWithPopup(SignIn.clerk, 'signIn', this.authenticateWithRedirectOrPopup, params, url => { popup.location.href = url.toString(); }); }; public authenticateWithWeb3 = async (params: AuthenticateWithWeb3Params): Promise => { const { identifier, generateSignature, strategy = 'web3_metamask_signature', walletName } = params || {}; const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; if (!(typeof generateSignature === 'function')) { clerkMissingOptionError('generateSignature'); } if (provider === 'solana' && !walletName) { clerkMissingOptionError('walletName'); } await this.create({ identifier }); const web3FirstFactor = this.supportedFirstFactors?.find(f => f.strategy === strategy) as Web3SignatureFactor; if (!web3FirstFactor) { clerkVerifyWeb3WalletCalledBeforeCreate('SignIn'); } await this.prepareFirstFactor(web3FirstFactor); const { message } = this.firstFactorVerification; if (!message) { clerkVerifyWeb3WalletCalledBeforeCreate('SignIn'); } let signature: string; try { signature = await generateSignature({ identifier, nonce: message, walletName, provider }); } catch (err) { // There is a chance that as a user when you try to setup and use the Coinbase Wallet with an existing // Passkey in order to authenticate, the initial generate signature request to be rejected. For this // reason we retry the request once more in order for the flow to be able to be completed successfully. // // error code 4001 means the user rejected the request // Reference: https://docs.cdp.coinbase.com/wallet-sdk/docs/errors if (provider === 'coinbase_wallet' && err.code === 4001) { signature = await generateSignature({ identifier, nonce: message, provider, walletName }); } else { throw err; } } return this.attemptFirstFactor({ signature, strategy, }); }; public authenticateWithMetamask = async (): Promise => { const identifier = await web3().getMetamaskIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: web3().generateSignatureWithMetamask, strategy: 'web3_metamask_signature', }); }; public authenticateWithCoinbaseWallet = async (): Promise => { const identifier = await web3().getCoinbaseWalletIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: web3().generateSignatureWithCoinbaseWallet, strategy: 'web3_coinbase_wallet_signature', }); }; public authenticateWithBase = async (): Promise => { const identifier = await web3().getBaseIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: web3().generateSignatureWithBase, strategy: 'web3_base_signature', }); }; public authenticateWithOKXWallet = async (): Promise => { const identifier = await web3().getOKXWalletIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: web3().generateSignatureWithOKXWallet, strategy: 'web3_okx_wallet_signature', }); }; /** * Authenticates a user using a Solana Web3 wallet during sign-in. * * @param params - Configuration for Solana authentication * @param params.walletName - The name of the Solana wallet to use for authentication * @returns A promise that resolves to the updated SignIn resource * @throws {ClerkRuntimeError} If walletName is not provided or wallet connection fails * * @example * ```typescript * await signIn.authenticateWithSolana({ walletName: 'phantom' }); * ``` */ public authenticateWithSolana = async ({ walletName, }: SignInAuthenticateWithSolanaParams): Promise => { const identifier = await web3().getSolanaIdentifier(walletName); return this.authenticateWithWeb3({ identifier, generateSignature: p => web3().generateSignatureWithSolana({ ...p, walletName: walletName }), strategy: 'web3_solana_signature', walletName: walletName, }); }; /** * Authenticates the sign-in with a passkey. * * When called without `params.flow` while the sign-in status is `needs_second_factor` (or * `needs_client_trust`) and the sign-in offers `passkey` among its `supportedSecondFactors`, the * passkey acts as the second factor: the in-progress sign-in is reused via the discrete * prepare/attempt second-factor flow. * * Otherwise the passkey verifies the first factor, with `params.flow` selecting how the ceremony * starts: `'autofill'`/`'discoverable'` create a new sign-in and identify the user from the * passkey itself, discarding any in-progress sign-in, while the default requires a sign-in * created beforehand. A sign-in parked at a second-factor status that does NOT offer passkey (the * backend advertises it only when the instance allows passkeys to satisfy the second factor, the * user has one registered, and the client version supports it) also takes this path, matching * clients that predate passkey second factors: the ceremony starts over instead of failing. * * Throws a `ClerkWebAuthnError` when WebAuthn is unsupported or the passkey ceremony fails. * @returns The updated `SignIn` resource. */ public authenticateWithPasskey = async (params?: AuthenticateWithPasskeyParams): Promise => { const { flow } = params || {}; /** * The UI should always prevent from this method being called if WebAuthn is not supported. * As a precaution we need to check if WebAuthn is supported. */ const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow; const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow; const isWebAuthnAutofillSupported = SignIn.clerk.__internal_isWebAuthnAutofillSupported || isWebAuthnAutofillSupportedOnWindow; if (!isWebAuthnSupported()) { throw new ClerkWebAuthnError('Passkeys are not supported', { code: 'passkey_not_supported', }); } const isSecondFactor = this.status === 'needs_second_factor' || this.status === 'needs_client_trust'; const hasPasskeySecondFactor = (this.supportedSecondFactors || []).some(f => f.strategy === 'passkey'); if (!flow && isSecondFactor && hasPasskeySecondFactor) { await this.prepareSecondFactor({ strategy: 'passkey' }); const { nonce: secondFactorNonce } = this.secondFactorVerification; const secondFactorPublicKeyOptions = secondFactorNonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(secondFactorNonce)) : null; if (!secondFactorPublicKeyOptions) { clerkMissingWebAuthnPublicKeyOptions('get'); } const { publicKeyCredential, error } = await webAuthnGetCredential({ publicKeyOptions: secondFactorPublicKeyOptions, conditionalUI: false, }); if (!publicKeyCredential) { throw error; } return this.attemptSecondFactor({ publicKeyCredential, strategy: 'passkey', }); } if (flow === 'autofill' || flow === 'discoverable') { // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level await this.create({ strategy: 'passkey' }); } else { // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level const passKeyFactor = this.supportedFirstFactors.find( // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level f => f.strategy === 'passkey', ) as PasskeyFactor; if (!passKeyFactor) { clerkVerifyPasskeyCalledBeforeCreate(); } // @ts-ignore As this is experimental we want to support it at runtime, but not at the type level await this.prepareFirstFactor(passKeyFactor); } const { nonce } = this.firstFactorVerification; const publicKeyOptions = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null; if (!publicKeyOptions) { clerkMissingWebAuthnPublicKeyOptions('get'); } let canUseConditionalUI = false; if (flow === 'autofill') { /** * If autofill is not supported gracefully handle the result, we don't need to throw. * The caller should always check this before calling this method. */ canUseConditionalUI = await isWebAuthnAutofillSupported(); } // Invoke the navigator.create.get() method. const { publicKeyCredential, error } = await webAuthnGetCredential({ publicKeyOptions, conditionalUI: canUseConditionalUI, }); if (!publicKeyCredential) { throw error; } return this.attemptFirstFactor({ publicKeyCredential, strategy: 'passkey', }); }; validatePassword: ReturnType = (password, cb) => { if (SignIn.clerk.__internal_environment?.userSettings.passwordSettings) { return createValidatePassword(loadZxcvbn(), { ...SignIn.clerk.__internal_environment?.userSettings.passwordSettings, validatePassword: true, })(password, cb); } }; protected fromJSON(data: SignInJSON | SignInJSONSnapshot | null): this { if (data) { this.id = data.id; this.status = data.status; this.supportedIdentifiers = data.supported_identifiers; this.identifier = data.identifier; this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null; this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null; this.ssoBypassFirstFactors = deepSnakeToCamel(data.sso_bypass_first_factors ?? null) as | SignInFirstFactor[] | null; this.firstFactorVerification = new Verification(data.first_factor_verification); this.secondFactorVerification = new Verification(data.second_factor_verification); this.createdSessionId = data.created_session_id; this.userData = new UserData(data.user_data); this.clientTrustState = data.client_trust_state ?? undefined; this.protectCheck = data.protect_check ? { status: data.protect_check.status, token: data.protect_check.token, sdkUrl: data.protect_check.sdk_url, expiresAt: data.protect_check.expires_at, uiHints: data.protect_check.ui_hints, } : null; this.timezone = data.timezone ?? null; } eventBus.emit('resource:update', { resource: this }); return this; } private clientBypass() { return SignIn.clerk.client?.captchaBypass; } /** * Determines whether captcha is required for sign in based on the provided params. * Add new conditions here as captcha requirements evolve. */ private shouldRequireCaptcha(params: SignInCreateParams): boolean { if ('signUpIfMissing' in params && params.signUpIfMissing) { return true; } return false; } /** * We delegate bot detection to the following providers, instead of relying on turnstile exclusively */ protected shouldBypassCaptchaForAttempt(params: SignInCreateParams) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass; // Check transfer strategy against bypass list if (params.transfer && SignIn.clerk.client?.signUp?.verifications?.externalAccount?.status === 'transferable') { const signUpStrategy = SignIn.clerk.client.signUp.verifications.externalAccount.strategy; return signUpStrategy ? captchaOauthBypass.some(strategy => strategy === signUpStrategy) : false; } // Check direct strategy against bypass list if ('strategy' in params && params.strategy) { return captchaOauthBypass.some(strategy => strategy === params.strategy); } return false; } public __internal_updateFromJSON(data: SignInJSON | SignInJSONSnapshot | null): this { return this.fromJSON(data); } public __internal_toSnapshot(): SignInJSONSnapshot { return { object: 'sign_in', id: this.id || '', status: this.status || null, supported_identifiers: this.supportedIdentifiers, supported_first_factors: deepCamelToSnake(this.supportedFirstFactors), supported_second_factors: deepCamelToSnake(this.supportedSecondFactors), sso_bypass_first_factors: deepCamelToSnake(this.ssoBypassFirstFactors) ?? undefined, first_factor_verification: this.firstFactorVerification.__internal_toSnapshot(), second_factor_verification: this.secondFactorVerification.__internal_toSnapshot(), identifier: this.identifier, created_session_id: this.createdSessionId, user_data: this.userData.__internal_toSnapshot(), timezone: this.timezone, protect_check: this.protectCheck ? { status: this.protectCheck.status, token: this.protectCheck.token, sdk_url: this.protectCheck.sdkUrl, ...(this.protectCheck.expiresAt !== undefined && { expires_at: this.protectCheck.expiresAt }), ...(this.protectCheck.uiHints !== undefined && { ui_hints: this.protectCheck.uiHints }), } : null, }; } } type SelectFirstFactorParams = | { strategy: 'email_code'; emailAddressId?: string; phoneNumberId?: never } | { strategy: 'email_link'; emailAddressId?: string; phoneNumberId?: never } | { strategy: 'phone_code'; phoneNumberId?: string; emailAddressId?: never }; class SignInFuture implements SignInFutureResource { emailCode = { sendCode: this.sendEmailCode.bind(this), verifyCode: this.verifyEmailCode.bind(this), }; emailLink = { sendLink: this.sendEmailLink.bind(this), waitForVerification: this.waitForEmailLinkVerification.bind(this), get verification() { if (!inBrowser()) { return null; } const status = getClerkQueryParam('__clerk_status') as 'verified' | 'expired' | 'failed' | 'client_mismatch'; const createdSessionId = getClerkQueryParam('__clerk_created_session'); if (!status || !createdSessionId) { return null; } const verifiedFromTheSameClient = status === 'verified' && typeof SignIn.clerk.client !== 'undefined' && SignIn.clerk.client.sessions.some(s => s.id === createdSessionId); return { status, createdSessionId, verifiedFromTheSameClient, }; }, }; resetPasswordEmailCode = { sendCode: this.sendResetPasswordEmailCode.bind(this), verifyCode: this.verifyResetPasswordEmailCode.bind(this), submitPassword: this.submitResetPassword.bind(this), }; resetPasswordPhoneCode = { sendCode: this.sendResetPasswordPhoneCode.bind(this), verifyCode: this.verifyResetPasswordPhoneCode.bind(this), submitPassword: this.submitResetPassword.bind(this), }; phoneCode = { sendCode: this.sendPhoneCode.bind(this), verifyCode: this.verifyPhoneCode.bind(this), }; mfa = { sendPhoneCode: this.sendMFAPhoneCode.bind(this), verifyPhoneCode: this.verifyMFAPhoneCode.bind(this), sendEmailCode: this.sendMFAEmailCode.bind(this), verifyEmailCode: this.verifyMFAEmailCode.bind(this), verifyTOTP: this.verifyTOTP.bind(this), verifyBackupCode: this.verifyBackupCode.bind(this), passkey: this.verifyMFAPasskey.bind(this), }; #canBeDiscarded = false; readonly #resource: SignIn; constructor(resource: SignIn) { this.#resource = resource; } get id() { return this.#resource.id; } get identifier() { return this.#resource.identifier; } get timezone() { return this.#resource.timezone; } get createdSessionId() { return this.#resource.createdSessionId; } get userData() { return this.#resource.userData; } get status() { // @TODO hooks-revamp: Consolidate this fallback val with stateProxy return this.#resource.status || 'needs_identifier'; } get supportedFirstFactors() { return this.#resource.supportedFirstFactors ?? []; } get supportedSecondFactors() { return this.#resource.supportedSecondFactors ?? []; } get ssoBypassFirstFactors() { return this.#resource.ssoBypassFirstFactors ?? []; } get isTransferable() { return this.#resource.firstFactorVerification.status === 'transferable'; } get existingSession() { if ( this.#resource.firstFactorVerification.status === 'failed' && this.#resource.firstFactorVerification.error?.code === 'identifier_already_signed_in' && this.#resource.firstFactorVerification.error?.meta?.sessionId ) { return { sessionId: this.#resource.firstFactorVerification.error?.meta?.sessionId }; } return undefined; } get firstFactorVerification() { return this.#resource.firstFactorVerification; } get secondFactorVerification() { return this.#resource.secondFactorVerification; } get protectCheck() { return this.#resource.protectCheck; } /** * Submits a proof token to resolve a Clerk Protect challenge (`protect_check`) during sign-in. * * @param params - The proof token parameters. * @param params.proofToken - The proof token produced by the Protect challenge SDK. * @returns A promise resolving to `{ error }` — `null` on success, otherwise the encountered error. */ async submitProtectCheck(params: SignInFutureSubmitProtectCheckParams): Promise<{ error: ClerkError | null }> { return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePatch({ action: 'protect_check', body: { proof_token: params.proofToken }, }); }); } get canBeDiscarded() { return this.#canBeDiscarded; } async sendResetPasswordEmailCode(): Promise<{ error: ClerkError | null }> { if (!this.#resource.id) { throw new Error('Cannot reset password without a sign in.'); } return runAsyncResourceTask(this.#resource, async () => { const resetPasswordEmailCodeFactor = this.#resource.supportedFirstFactors?.find( f => f.strategy === 'reset_password_email_code', ); if (!resetPasswordEmailCodeFactor) { throw new ClerkRuntimeError('Reset password email code factor not found', { code: 'factor_not_found', }); } const { emailAddressId } = resetPasswordEmailCodeFactor; await this.#resource.__internal_basePost({ body: { emailAddressId, strategy: 'reset_password_email_code' }, action: 'prepare_first_factor', coalesce: true, }); }); } async verifyResetPasswordEmailCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'reset_password_email_code' }, action: 'attempt_first_factor', }); }); } async sendResetPasswordPhoneCode( params: SignInFutureResetPasswordPhoneCodeSendParams = {}, ): Promise<{ error: ClerkError | null }> { const { phoneNumber } = params; if (!this.#resource.id && !phoneNumber) { throw new Error( 'signIn.resetPasswordPhoneCode.sendCode() cannot be called without a phoneNumber if an existing signIn does not exist.', ); } return runAsyncResourceTask(this.#resource, async () => { if (phoneNumber) { await this._create({ identifier: phoneNumber }); } const resetPasswordPhoneCodeFactor = this.#resource.supportedFirstFactors?.find( f => f.strategy === 'reset_password_phone_code', ); if (!resetPasswordPhoneCodeFactor) { throw new ClerkRuntimeError('Reset password phone code factor not found', { code: 'factor_not_found', }); } const { phoneNumberId } = resetPasswordPhoneCodeFactor; await this.#resource.__internal_basePost({ body: { phoneNumberId, strategy: 'reset_password_phone_code' }, action: 'prepare_first_factor', coalesce: true, }); }); } async verifyResetPasswordPhoneCode( params: SignInFutureResetPasswordPhoneCodeVerifyParams, ): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'reset_password_phone_code' }, action: 'attempt_first_factor', }); }); } async submitResetPassword(params: SignInFutureResetPasswordSubmitParams): Promise<{ error: ClerkError | null }> { const { password, signOutOfOtherSessions = true } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { password, signOutOfOtherSessions }, action: 'reset_password', }); }); } /** * Determines whether captcha is required for sign in based on the provided params. * Add new conditions here as captcha requirements evolve. */ private shouldRequireCaptcha(params: { signUpIfMissing?: boolean }): boolean { if (params.signUpIfMissing) { return true; } return false; } private shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass; // Check transfer strategy against bypass list if (params.transfer && SignIn.clerk.client?.signUp?.verifications?.externalAccount?.status === 'transferable') { const signUpStrategy = SignIn.clerk.client.signUp.verifications.externalAccount.strategy; return signUpStrategy ? captchaOauthBypass.some(strategy => strategy === signUpStrategy) : false; } // Check direct strategy against bypass list if (params.strategy) { return captchaOauthBypass.some(strategy => strategy === params.strategy); } return false; } private async getCaptchaToken( params: { strategy?: string; transfer?: boolean; signUpIfMissing?: boolean } = {}, ): Promise<{ captchaToken?: string; captchaWidgetType?: CaptchaWidgetType; captchaError?: unknown; }> { if ( !this.shouldRequireCaptcha(params) || __BUILD_DISABLE_RHC__ || SignIn.clerk.client?.captchaBypass || this.shouldBypassCaptchaForAttempt(params) ) { return { captchaToken: undefined, captchaWidgetType: undefined, captchaError: undefined, }; } const captchaChallenge = new CaptchaChallenge(SignIn.clerk); const response = await captchaChallenge.managedOrInvisible({ action: 'signin' }); if (!response) { throw new Error('Captcha challenge failed'); } const { captchaError, captchaToken, captchaWidgetType } = response; return { captchaToken, captchaWidgetType, captchaError }; } private async _create(params: SignInFutureCreateParams, options?: { resolveProtectCheck?: boolean }): Promise { const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params); const timezone = params.timezone ?? getBrowserTimezone(); const body: Record = { ...params, captchaToken, captchaWidgetType, captchaError, locale: getBrowserLocale() || undefined, ...(timezone !== null ? { timezone } : {}), }; await this.#resource.__internal_basePost( { path: this.#resource.pathRoot, body, }, options, ); } async create(params: SignInFutureCreateParams): Promise<{ error: ClerkError | null }> { return runAsyncResourceTask(this.#resource, async () => { await this._create(params); }); } async password(params: SignInFuturePasswordParams): Promise<{ error: ClerkError | null }> { if ([params.identifier, params.emailAddress, params.phoneNumber].filter(Boolean).length > 1) { throw new Error('Only one of identifier, emailAddress, or phoneNumber can be provided'); } return runAsyncResourceTask(this.#resource, async () => { // TODO @userland-errors: const identifier = params.identifier || params.emailAddress || params.phoneNumber; const previousIdentifier = this.#resource.identifier; const locale = getBrowserLocale(); const timezone = params.timezone ?? this.#resource.timezone ?? getBrowserTimezone(); await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body: { identifier: identifier || previousIdentifier, password: params.password, ...(locale ? { locale } : {}), ...(timezone !== null ? { timezone } : {}), }, }); }); } async sendEmailCode(params: SignInFutureEmailCodeSendParams = {}): Promise<{ error: ClerkError | null }> { const { emailAddress, emailAddressId } = params; if (!this.#resource.id && emailAddressId) { throw new Error( 'signIn.emailCode.sendCode() cannot be called with an emailAddressId if an existing signIn does not exist.', ); } if (!this.#resource.id && !emailAddress) { throw new Error( 'signIn.emailCode.sendCode() cannot be called without an emailAddress if an existing signIn does not exist.', ); } return runAsyncResourceTask(this.#resource, async () => { if (emailAddress) { await this._create({ identifier: emailAddress }); } const emailCodeFactor = this.selectFirstFactor({ strategy: 'email_code', emailAddressId }); if (!emailCodeFactor) { throw new ClerkRuntimeError('Email code factor not found', { code: 'factor_not_found' }); } await this.#resource.__internal_basePost({ body: { emailAddressId: emailCodeFactor.emailAddressId, strategy: 'email_code' }, action: 'prepare_first_factor', coalesce: true, }); }); } async verifyEmailCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'email_code' }, action: 'attempt_first_factor', }); }); } async sendEmailLink(params: SignInFutureEmailLinkSendParams): Promise<{ error: ClerkError | null }> { const { emailAddress, verificationUrl, emailAddressId } = params; if (!this.#resource.id && emailAddressId) { throw new Error( 'signIn.emailLink.sendLink() cannot be called with an emailAddressId if an existing signIn does not exist.', ); } if (!this.#resource.id && !emailAddress) { throw new Error( 'signIn.emailLink.sendLink() cannot be called without an emailAddress if an existing signIn does not exist.', ); } return runAsyncResourceTask(this.#resource, async () => { if (emailAddress) { await this._create({ identifier: emailAddress }); } const emailLinkFactor = this.selectFirstFactor({ strategy: 'email_link', emailAddressId }); if (!emailLinkFactor) { throw new ClerkRuntimeError('Email link factor not found', { code: 'factor_not_found' }); } let absoluteVerificationUrl = verificationUrl; try { new URL(verificationUrl); } catch { absoluteVerificationUrl = window.location.origin + verificationUrl; } await this.#resource.__internal_basePost({ body: { emailAddressId: emailLinkFactor.emailAddressId, redirectUrl: absoluteVerificationUrl, strategy: 'email_link', }, action: 'prepare_first_factor', coalesce: true, }); }); } async waitForEmailLinkVerification(): Promise<{ error: ClerkError | null }> { return runAsyncResourceTask(this.#resource, async () => { const { run, stop } = Poller(); await new Promise((resolve, reject) => { void run(async () => { try { const res = await this.#resource.__internal_baseGet(); if (isTerminalEmailLinkVerificationStatus(res.firstFactorVerification.status)) { stop(); resolve(res); } } catch (err) { stop(); reject(err); } }); }); }); } async sendPhoneCode(params: SignInFuturePhoneCodeSendParams = {}): Promise<{ error: ClerkError | null }> { const { phoneNumber, phoneNumberId, channel = 'sms' } = params; if (!this.#resource.id && phoneNumberId) { throw new Error( 'signIn.phoneCode.sendCode() cannot be called with an phoneNumberId if an existing signIn does not exist.', ); } if (!this.#resource.id && !phoneNumber) { throw new Error( 'signIn.phoneCode.sendCode() cannot be called without an phoneNumber if an existing signIn does not exist.', ); } return runAsyncResourceTask(this.#resource, async () => { if (phoneNumber) { await this._create({ identifier: phoneNumber }); } const phoneCodeFactor = this.selectFirstFactor({ strategy: 'phone_code', phoneNumberId }); if (!phoneCodeFactor) { throw new ClerkRuntimeError('Phone code factor not found', { code: 'factor_not_found' }); } await this.#resource.__internal_basePost({ body: { phoneNumberId: phoneCodeFactor.phoneNumberId, strategy: 'phone_code', channel }, action: 'prepare_first_factor', coalesce: true, }); }); } async verifyPhoneCode(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'phone_code' }, action: 'attempt_first_factor', }); }); } async sso(params: SignInFutureSSOParams): Promise<{ error: ClerkError | null }> { const { strategy, redirectUrl, redirectCallbackUrl, popup, oidcPrompt, slackTeamIdHint, enterpriseConnectionId, identifier, } = params; return runAsyncResourceTask(this.#resource, async () => { let actionCompleteRedirectUrl = redirectUrl; try { new URL(redirectUrl); } catch { actionCompleteRedirectUrl = window.location.origin + redirectUrl; } const routes = { redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectCallbackUrl), actionCompleteRedirectUrl }; if (popup) { const wrappedRoutes = wrapWithPopupRoutes(SignIn.clerk, { redirectCallbackUrl: routes.redirectUrl, redirectUrl: actionCompleteRedirectUrl, }); routes.redirectUrl = wrappedRoutes.redirectCallbackUrl; routes.actionCompleteRedirectUrl = wrappedRoutes.redirectUrl; } // Reuse the existing sign-in by default so any state already attached to it carries // into the SSO attempt (e.g. a ticket from `signIn.create({ strategy: 'ticket' })`, // or a discovered identifier). For OAuth, a pending redirect may belong to an // abandoned attempt, including a retry of the same provider (SDK-75), so start // fresh rather than replay it. `enterprise_sso` can reuse the sign-in because // `prepare_first_factor` below refreshes its redirect. When reusing a sign-in // without a pending redirect, an explicit `slackTeamIdHint` also triggers preparation // so the hint is applied to the existing attempt. const hasPendingRedirect = !!this.#resource.firstFactorVerification.externalVerificationRedirectURL; const wouldReplayStaleRedirect = strategy !== 'enterprise_sso' && hasPendingRedirect; const shouldCreateSignIn = !this.#resource.id || wouldReplayStaleRedirect; const isChallengePending = () => !!this.#resource.protectCheck || this.#resource.status === 'needs_protect_check'; const pendingHandOff = () => { const { status, externalVerificationRedirectURL } = this.#resource.firstFactorVerification; return status === 'unverified' ? externalVerificationRedirectURL : null; }; const resolveChallenge = () => ProtectCheckGate.getInstance().resolve(SignIn.clerk, this.#resource); let challengedCreateHandOff: URL | null = null; if (shouldCreateSignIn) { await this._create( { strategy, ...routes, identifier, oidcPrompt, slackTeamIdHint, }, { resolveProtectCheck: false }, ); if (isChallengePending()) { challengedCreateHandOff = pendingHandOff(); if (!challengedCreateHandOff) { await resolveChallenge(); } } } if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamIdHint !== undefined)) { const prepare = (options?: { resolveProtectCheck?: boolean }) => this.#resource.__internal_basePost( { body: { ...routes, oidcPrompt, slackTeamIdHint, enterpriseConnectionId, strategy, }, action: 'prepare_first_factor', coalesce: true, }, options, ); await prepare({ resolveProtectCheck: false }); if (isChallengePending() && !challengedCreateHandOff) { await resolveChallenge(); await prepare(); } } const externalVerificationRedirectURL = challengedCreateHandOff ?? pendingHandOff(); if (externalVerificationRedirectURL) { if (popup) { await _futureAuthenticateWithPopup(SignIn.clerk, { popup, externalVerificationRedirectURL }); // Pick up the modified SignIn resource await this.#resource.reload(); await resolveChallenge(); } else { SignIn.clerk.__internal_windowNavigate(externalVerificationRedirectURL); } } }); } async web3(params: SignInFutureWeb3Params): Promise<{ error: ClerkError | null }> { const { strategy } = params; const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; return runAsyncResourceTask(this.#resource, async () => { let identifier; let generateSignature: GenerateSignature; switch (provider) { case 'metamask': identifier = await web3().getMetamaskIdentifier(); generateSignature = web3().generateSignatureWithMetamask; break; case 'coinbase_wallet': identifier = await web3().getCoinbaseWalletIdentifier(); generateSignature = web3().generateSignatureWithCoinbaseWallet; break; case 'base': identifier = await web3().getBaseIdentifier(); generateSignature = web3().generateSignatureWithBase; break; case 'okx_wallet': identifier = await web3().getOKXWalletIdentifier(); generateSignature = web3().generateSignatureWithOKXWallet; break; case 'solana': if (!params.walletName) { throw new ClerkRuntimeError('Wallet name is required for Solana authentication.', { code: 'web3_solana_wallet_name_required', }); } identifier = await web3().getSolanaIdentifier(params.walletName); generateSignature = p => web3().generateSignatureWithSolana({ ...p, walletName: params.walletName as string }); break; default: throw new Error(`Unsupported Web3 provider: ${provider}`); } await this._create({ identifier }); const web3FirstFactor = this.#resource.supportedFirstFactors?.find( f => f.strategy === strategy, ) as Web3SignatureFactor; if (!web3FirstFactor) { throw new ClerkRuntimeError('Web3 first factor not found', { code: 'factor_not_found' }); } await this.#resource.__internal_basePost({ body: { web3WalletId: web3FirstFactor.web3WalletId, strategy }, action: 'prepare_first_factor', coalesce: true, }); const { message } = this.firstFactorVerification; if (!message) { throw new ClerkRuntimeError('Web3 nonce not found', { code: 'web3_nonce_not_found' }); } let signature: string; try { signature = await generateSignature({ identifier, nonce: message, walletName: params?.walletName, provider, }); } catch (err) { // There is a chance that as a user when you try to setup and use the Coinbase Wallet with an existing // Passkey in order to authenticate, the initial generate signature request to be rejected. For this // reason we retry the request once more in order for the flow to be able to be completed successfully. // // error code 4001 means the user rejected the request // Reference: https://docs.cdp.coinbase.com/wallet-sdk/docs/errors if (provider === 'coinbase_wallet' && err.code === 4001) { signature = await generateSignature({ identifier, nonce: message, provider, }); } else { throw err; } } await this.#resource.__internal_basePost({ body: { signature, strategy }, action: 'attempt_first_factor', }); }); } async passkey(params?: SignInFuturePasskeyParams): Promise<{ error: ClerkError | null }> { const { flow } = params || {}; /** * The UI should always prevent from this method being called if WebAuthn is not supported. * As a precaution we need to check if WebAuthn is supported. */ const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow; const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow; const isWebAuthnAutofillSupported = SignIn.clerk.__internal_isWebAuthnAutofillSupported || isWebAuthnAutofillSupportedOnWindow; if (!isWebAuthnSupported()) { throw new ClerkWebAuthnError('Passkeys are not supported', { code: 'passkey_not_supported', }); } return runAsyncResourceTask(this.#resource, async () => { if (flow === 'autofill' || flow === 'discoverable') { await this._create({ strategy: 'passkey' }); } else { const passKeyFactor = this.supportedFirstFactors.find(f => f.strategy === 'passkey') as PasskeyFactor; if (!passKeyFactor) { throw new ClerkRuntimeError('Passkey factor not found', { code: 'factor_not_found' }); } await this.#resource.__internal_basePost({ body: { strategy: 'passkey' }, action: 'prepare_first_factor', coalesce: true, }); } const { nonce } = this.firstFactorVerification; const publicKeyOptions = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null; if (!publicKeyOptions) { throw new ClerkRuntimeError('Missing public key options', { code: 'missing_public_key_options' }); } let canUseConditionalUI = false; if (flow === 'autofill') { /** * If autofill is not supported gracefully handle the result, we don't need to throw. * The caller should always check this before calling this method. */ canUseConditionalUI = await isWebAuthnAutofillSupported(); } // Invoke the navigator.create.get() method. const { publicKeyCredential, error } = await webAuthnGetCredential({ publicKeyOptions, conditionalUI: canUseConditionalUI, }); if (!publicKeyCredential) { throw new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_failed' }); } await this.#resource.__internal_basePost({ body: { publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(publicKeyCredential)), strategy: 'passkey', }, action: 'attempt_first_factor', }); }); } async sendMFAPhoneCode(): Promise<{ error: ClerkError | null }> { return runAsyncResourceTask(this.#resource, async () => { const phoneCodeFactor = this.#resource.supportedSecondFactors?.find(f => f.strategy === 'phone_code'); if (!phoneCodeFactor) { throw new ClerkRuntimeError('Phone code factor not found', { code: 'factor_not_found' }); } const { phoneNumberId } = phoneCodeFactor; await this.#resource.__internal_basePost({ body: { phoneNumberId, strategy: 'phone_code' }, action: 'prepare_second_factor', coalesce: true, }); }); } async verifyMFAPhoneCode(params: SignInFutureMFAPhoneCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'phone_code' }, action: 'attempt_second_factor', }); }); } async sendMFAEmailCode(): Promise<{ error: ClerkError | null }> { return runAsyncResourceTask(this.#resource, async () => { const emailCodeFactor = this.#resource.supportedSecondFactors?.find(f => f.strategy === 'email_code'); if (!emailCodeFactor) { throw new ClerkRuntimeError('Email code factor not found', { code: 'factor_not_found' }); } const { emailAddressId } = emailCodeFactor; await this.#resource.__internal_basePost({ body: { emailAddressId, strategy: 'email_code' }, action: 'prepare_second_factor', coalesce: true, }); }); } async verifyMFAEmailCode(params: SignInFutureMFAEmailCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'email_code' }, action: 'attempt_second_factor', }); }); } async verifyTOTP(params: SignInFutureTOTPVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'totp' }, action: 'attempt_second_factor', }); }); } async verifyBackupCode(params: SignInFutureBackupCodeVerifyParams): Promise<{ error: ClerkError | null }> { const { code } = params; return runAsyncResourceTask(this.#resource, async () => { await this.#resource.__internal_basePost({ body: { code, strategy: 'backup_code' }, action: 'attempt_second_factor', }); }); } async verifyMFAPasskey(): Promise<{ error: ClerkError | null }> { /** * The UI should always prevent from this method being called if WebAuthn is not supported. * As a precaution we need to check if WebAuthn is supported. */ const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow; const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow; if (!isWebAuthnSupported()) { throw new ClerkWebAuthnError('Passkeys are not supported', { code: 'passkey_not_supported', }); } return runAsyncResourceTask(this.#resource, async () => { const passkeyFactor = this.#resource.supportedSecondFactors?.find(f => f.strategy === 'passkey'); if (!passkeyFactor) { throw new ClerkRuntimeError('Passkey factor not found', { code: 'factor_not_found' }); } await this.#resource.__internal_basePost({ body: { strategy: 'passkey' }, action: 'prepare_second_factor', coalesce: true, }); const { nonce } = this.#resource.secondFactorVerification; const publicKeyOptions = nonce ? convertJSONToPublicKeyRequestOptions(JSON.parse(nonce)) : null; if (!publicKeyOptions) { throw new ClerkRuntimeError('Missing public key options', { code: 'missing_public_key_options' }); } const { publicKeyCredential, error } = await webAuthnGetCredential({ publicKeyOptions, conditionalUI: false, }); if (!publicKeyCredential) { throw new ClerkWebAuthnError(error.message, { code: 'passkey_retrieval_failed' }); } await this.#resource.__internal_basePost({ body: { publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(publicKeyCredential)), strategy: 'passkey', }, action: 'attempt_second_factor', }); }); } async ticket(params?: SignInFutureTicketParams): Promise<{ error: ClerkError | null }> { const ticket = params?.ticket ?? getClerkQueryParam('__clerk_ticket'); return this.create({ strategy: 'ticket', ticket: ticket ?? undefined }); } async finalize(params?: SignInFutureFinalizeParams): Promise<{ error: ClerkError | null }> { const { navigate } = params || {}; if (!this.#resource.createdSessionId) { throw new Error('Cannot finalize sign-in without a created session.'); } return runAsyncResourceTask(this.#resource, async () => { // Reload the client if the created session is not in the client's sessions. This can happen during modal SSO // flows where the in-memory client does not have the created session. if (SignIn.clerk.client && !SignIn.clerk.client.sessions.some(s => s.id === this.#resource.createdSessionId)) { await SignIn.clerk.client.reload(); } this.#canBeDiscarded = true; await SignIn.clerk.setActive({ session: this.#resource.createdSessionId, navigate }); }); } /** * Resets the current sign-in attempt by clearing all local state back to null. * Unlike other methods, this does NOT emit resource:fetch with 'fetching' status, * allowing for smooth UI transitions without loading states. */ reset(): Promise<{ error: ClerkError | null }> { if (!SignIn.clerk.client) { throw new Error('Cannot reset sign-in without a client.'); } this.#canBeDiscarded = true; SignIn.clerk.client.resetSignIn(); return Promise.resolve({ error: null }); } private selectFirstFactor( params: Extract, ): EmailCodeFactor | null; private selectFirstFactor( params: Extract, ): EmailLinkFactor | null; private selectFirstFactor( params: Extract, ): PhoneCodeFactor | null; private selectFirstFactor({ strategy, emailAddressId, phoneNumberId, }: SelectFirstFactorParams): EmailCodeFactor | EmailLinkFactor | PhoneCodeFactor | null { if (!this.#resource.supportedFirstFactors) { return null; } if (emailAddressId) { const factor = this.#resource.supportedFirstFactors.find( f => f.strategy === strategy && f.emailAddressId === emailAddressId, ) as EmailCodeFactor | EmailLinkFactor; if (factor) { return factor; } } if (phoneNumberId) { const factor = this.#resource.supportedFirstFactors.find( f => f.strategy === strategy && f.phoneNumberId === phoneNumberId, ) as PhoneCodeFactor; if (factor) { return factor; } } // Try to find a factor that matches the identifier. const factorForIdentifier = this.#resource.supportedFirstFactors.find( f => f.strategy === strategy && f.safeIdentifier === this.#resource.identifier, ) as EmailCodeFactor | EmailLinkFactor | PhoneCodeFactor; if (factorForIdentifier) { return factorForIdentifier; } // If no factor is found matching the identifier, try to find a factor that matches the strategy. const factorForStrategy = this.#resource.supportedFirstFactors.find(f => f.strategy === strategy) as | EmailCodeFactor | EmailLinkFactor | PhoneCodeFactor; if (factorForStrategy) { return factorForStrategy; } return null; } }