diff --git a/.changeset/twelve-fans-smell.md b/.changeset/twelve-fans-smell.md new file mode 100644 index 00000000000..85ce6fe132f --- /dev/null +++ b/.changeset/twelve-fans-smell.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Signal MFA support diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index c7a066494bc..74207e62155 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -27,16 +27,19 @@ import type { SamlConfig, SignInCreateParams, SignInFirstFactor, + SignInFutureBackupCodeVerifyParams, SignInFutureCreateParams, SignInFutureEmailCodeSendParams, SignInFutureEmailCodeVerifyParams, SignInFutureFinalizeParams, + SignInFutureMFAPhoneCodeVerifyParams, SignInFuturePasswordParams, SignInFuturePhoneCodeSendParams, SignInFuturePhoneCodeVerifyParams, SignInFutureResetPasswordSubmitParams, SignInFutureResource, SignInFutureSSOParams, + SignInFutureTOTPVerifyParams, SignInIdentifier, SignInJSON, SignInJSONSnapshot, @@ -515,6 +518,13 @@ class SignInFuture implements SignInFutureResource { verifyCode: this.verifyPhoneCode.bind(this), }; + mfa = { + sendPhoneCode: this.sendMFAPhoneCode.bind(this), + verifyPhoneCode: this.verifyMFAPhoneCode.bind(this), + verifyTOTP: this.verifyTOTP.bind(this), + verifyBackupCode: this.verifyBackupCode.bind(this), + }; + constructor(readonly resource: SignIn) {} get status() { @@ -688,6 +698,52 @@ class SignInFuture implements SignInFutureResource { }); } + async sendMFAPhoneCode(): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + const phoneCodeFactor = this.resource.supportedSecondFactors?.find(f => f.strategy === 'phone_code'); + + if (!phoneCodeFactor) { + throw new Error('Phone code factor not found'); + } + + const { phoneNumberId } = phoneCodeFactor; + await this.resource.__internal_basePost({ + body: { phoneNumberId, strategy: 'phone_code' }, + action: 'prepare_second_factor', + }); + }); + } + + async verifyMFAPhoneCode(params: SignInFutureMFAPhoneCodeVerifyParams): Promise<{ error: unknown }> { + const { code } = params; + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { code, strategy: 'phone_code' }, + action: 'attempt_second_factor', + }); + }); + } + + async verifyTOTP(params: SignInFutureTOTPVerifyParams): Promise<{ error: unknown }> { + 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: unknown }> { + const { code } = params; + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { code, strategy: 'backup_code' }, + action: 'attempt_second_factor', + }); + }); + } + async finalize(params?: SignInFutureFinalizeParams): Promise<{ error: unknown }> { const { navigate } = params || {}; return runAsyncResourceTask(this.resource, async () => { diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index ca4a2ca8e1c..57d4cb76187 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -57,6 +57,12 @@ export class StateProxy implements State { 'submitPassword', ] as const), phoneCode: this.wrapMethods(() => target().phoneCode, ['sendCode', 'verifyCode'] as const), + mfa: this.wrapMethods(() => target().mfa, [ + 'sendPhoneCode', + 'verifyPhoneCode', + 'verifyTOTP', + 'verifyBackupCode', + ] as const), }, }; } diff --git a/packages/types/src/signInFuture.ts b/packages/types/src/signInFuture.ts index cc4310cc78e..8f8cffee02c 100644 --- a/packages/types/src/signInFuture.ts +++ b/packages/types/src/signInFuture.ts @@ -51,6 +51,18 @@ export interface SignInFutureSSOParams { redirectCallbackUrl: string; } +export interface SignInFutureMFAPhoneCodeVerifyParams { + code: string; +} + +export interface SignInFutureTOTPVerifyParams { + code: string; +} + +export interface SignInFutureBackupCodeVerifyParams { + code: string; +} + export interface SignInFutureFinalizeParams { navigate?: SetActiveNavigate; } @@ -76,5 +88,11 @@ export interface SignInFutureResource { submitPassword: (params: SignInFutureResetPasswordSubmitParams) => Promise<{ error: unknown }>; }; sso: (params: SignInFutureSSOParams) => Promise<{ error: unknown }>; + mfa: { + sendPhoneCode: () => Promise<{ error: unknown }>; + verifyPhoneCode: (params: SignInFutureMFAPhoneCodeVerifyParams) => Promise<{ error: unknown }>; + verifyTOTP: (params: SignInFutureTOTPVerifyParams) => Promise<{ error: unknown }>; + verifyBackupCode: (params: SignInFutureBackupCodeVerifyParams) => Promise<{ error: unknown }>; + }; finalize: (params?: SignInFutureFinalizeParams) => Promise<{ error: unknown }>; }