Skip to main content
Docs

Build a custom sign-in flow with multi-factor authentication

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Important

This guide applies to the following Clerk SDKs:

  • @clerk/react v6 or higher
  • @clerk/nextjs v7 or higher
  • @clerk/expo v3 or higher
  • @clerk/react-router v3 or higher
  • @clerk/tanstack-react-start v0.26.0 or higher

If you're using an older version of one of these SDKs, or are using the legacy API, refer to the legacy API documentation.

Multi-factor verification (MFA) is an added layer of security that requires users to provide a second verification factor to access an account.

Clerk supports second factor verification through SMS verification code, Authenticator application, and Backup codes.

This guide will walk you through how to build a custom email/password sign-in flow that supports Authenticator application and Backup codes as the second factor.

Enable email and password

This guide uses email and password to sign in, however, you can modify this approach according to the needs of your application.

To follow this guide, you first need to ensure email and password are enabled for your application.

  1. In the Clerk Dashboard, navigate to the User & authentication page.
  2. Enable Sign-in with email.
  3. Select the Password tab and enable Sign-up with password. Leave Require a password at sign-up enabled.

Enable multi-factor authentication

For your users to be able to enable MFA for their account, you need to enable MFA for your application.

  1. In the Clerk Dashboard, navigate to the Multi-factor page.
  2. For the purpose of this guide, toggle on both the Authenticator application and Backup codes strategies.
  3. Select Save.

Sign-in flow

Signing in to an MFA-enabled account is identical to the regular sign-in process. However, in the case of an MFA-enabled account, a sign-in won't convert until both first factor and second factor verifications are completed.

To authenticate a user using their email and password, you need to:

  1. Initiate the sign-up process by collecting the user's email address and password with the method.
  2. Collect the TOTP code and verify it with the method.
  3. If the TOTP verification is successful, finalize the sign-in with the method to set the newly created session as the active session.

Tip

For this example to work, the user must have MFA enabled on their account. You need to add the ability for your users to manage their MFA settings. See the manage SMS-based MFA or the manage TOTP-based MFA guide, depending on your needs.

app/sign-in/page.tsx
'use client'

import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function SignInForm() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()

  const handleSubmit = async (formData: FormData) => {
    const emailAddress = formData.get('email') as string
    const password = formData.get('password') as string

    await signIn.password({
      emailAddress,
      password,
    })
  }

  const handleSubmitTOTP = async (formData: FormData) => {
    const code = formData.get('code') as string
    const useBackupCode = formData.get('useBackupCode') === 'on'

    if (useBackupCode) {
      await signIn.mfa.verifyBackupCode({ code })
    } else {
      await signIn.mfa.verifyTOTP({ code })
    }

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: () => {
          router.push('/')
        },
      })
    }
  }

  if (signIn.status === 'needs_second_factor') {
    return (
      <div>
        <h1>Verify your account</h1>
        <form action={handleSubmitTOTP}>
          <div>
            <label htmlFor="code">Code</label>
            <input id="code" name="code" type="text" />
            {errors.fields.code && <p>{errors.fields.code.message}</p>}
          </div>
          <div>
            <label>
              Use backup code
              <input type="checkbox" name="useBackupCode" />
            </label>
          </div>
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify
          </button>
        </form>
      </div>
    )
  }

  return (
    <>
      <h1>Sign in</h1>
      <form action={handleSubmit}>
        <div>
          <label htmlFor="emailAddress">Email</label>
          <input id="emailAddress" name="emailAddress" type="emailAddress" />
          {errors.fields.emailAddress && <p>{errors.fields.emailAddress.message}</p>}
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" name="password" type="password" />
          {errors.fields.password && <p>{errors.fields.password.message}</p>}
        </div>
        <button type="submit" disabled={fetchStatus === 'fetching'}>
          Continue
        </button>
      </form>
    </>
  )
}

Next steps

Now that users can sign in with MFA, you need to add the ability for your users to manage their MFA settings. Learn how to build a custom flow for managing TOTP MFA or for managing SMS MFA.

Feedback

What did you think of this content?

Last updated on