Build a custom flow for managing SMS-based multi-factor authentication
Multi-factor verification (MFA) is an added layer of security that requires users to provide a second verification factor to access an account.
One of the options that Clerk supports for MFA is SMS verification codes. This guide will walk you through how to build a custom flow that allows users to manage their TOTP settings.
Enable multi-factor authentication
For your users to be able to enable MFA for their account, you need to enable MFA as an MFA authentication strategy in your Clerk application.
- In the Clerk Dashboard, navigate to the Multi-factor page.
- Enable SMS verification code and Backup codes and select Save.
Create the multi-factor management flow
This example is written for Next.js App Router but it can be adapted for any React-based framework.
This example consists of two pages:
- The main page where users can manage their SMS MFA settings.
- The page where users can add a phone number to their account.
Use the following tabs to view the code necessary for each page.
'use client'
import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import { BackupCodeResource, PhoneNumberResource } from '@clerk/types'
import Link from 'next/link'
// Display phone numbers reserved for MFA
const ManageMfaPhoneNumbers = () => {
  const { user } = useUser()
  if (!user) return null
  // Check if any phone numbers are reserved for MFA
  const mfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => ph.reservedForSecondFactor)
    .sort((ph: PhoneNumberResource) => (ph.defaultSecondFactor ? -1 : 1))
  if (mfaPhones.length === 0) {
    return <p>There are currently no phone numbers reserved for MFA.</p>
  }
  return (
    <>
      <h2>Phone numbers reserved for MFA</h2>
      <ul>
        {mfaPhones.map((phone) => {
          return (
            <li key={phone.id} style={{ display: 'flex', gap: '10px' }}>
              <p>
                {phone.phoneNumber} {phone.defaultSecondFactor && '(Default)'}
              </p>
              <div>
                <button onClick={() => phone.setReservedForSecondFactor({ reserved: false })}>
                  Disable for MFA
                </button>
              </div>
              {!phone.defaultSecondFactor && (
                <div>
                  <button onClick={() => phone.makeDefaultSecondFactor()}>Make default</button>
                </div>
              )}
              <div>
                <button onClick={() => phone.destroy()}>Remove from account</button>
              </div>
            </li>
          )
        })}
      </ul>
    </>
  )
}
// Display phone numbers that are not reserved for MFA
const ManageAvailablePhoneNumbers = () => {
  const { user } = useUser()
  const setReservedForSecondFactor = useReverification((phone: PhoneNumberResource) =>
    phone.setReservedForSecondFactor({ reserved: true }),
  )
  const destroyPhone = useReverification((phone: PhoneNumberResource) => phone.destroy())
  if (!user) return null
  // Check if any phone numbers aren't reserved for MFA
  const availableForMfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => !ph.reservedForSecondFactor)
  // Reserve a phone number for MFA
  const reservePhoneForMfa = async (phone: PhoneNumberResource) => {
    // Set the phone number as reserved for MFA
    await setReservedForSecondFactor(phone)
    // Refresh the user information to reflect changes
    await user.reload()
  }
  if (availableForMfaPhones.length === 0) {
    return <p>There are currently no verified phone numbers available to be reserved for MFA.</p>
  }
  return (
    <>
      <h2>Phone numbers that are not reserved for MFA</h2>
      <ul>
        {availableForMfaPhones.map((phone) => {
          return (
            <li key={phone.id} style={{ display: 'flex', gap: '10px' }}>
              <p>{phone.phoneNumber}</p>
              <div>
                <button onClick={() => reservePhoneForMfa(phone)}>Use for MFA</button>
              </div>
              <div>
                <button onClick={() => destroyPhone()}>Remove from account</button>
              </div>
            </li>
          )
        })}
      </ul>
    </>
  )
}
// Generate and display backup codes
function GenerateBackupCodes() {
  const { user } = useUser()
  const [backupCodes, setBackupCodes] = React.useState<BackupCodeResource | undefined>(undefined)
  const createBackupCode = useReverification(() => user?.createBackupCode())
  const [loading, setLoading] = React.useState(false)
  React.useEffect(() => {
    if (backupCodes) {
      return
    }
    setLoading(true)
    void createBackupCode()
      .then((backupCode: BackupCodeResource) => {
        setBackupCodes(backupCode)
        setLoading(false)
      })
      .catch((err) => {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(JSON.stringify(err, null, 2))
        setLoading(false)
      })
  }, [])
  if (loading) {
    return <p>Loading...</p>
  }
  if (!backupCodes) {
    return <p>There was a problem generating backup codes</p>
  }
  return (
    <ol>
      {backupCodes.codes.map((code, index) => (
        <li key={index}>{code}</li>
      ))}
    </ol>
  )
}
export default function ManageSMSMFA() {
  const [showBackupCodes, setShowBackupCodes] = React.useState(false)
  const { isLoaded, user } = useUser()
  if (!isLoaded) return null
  if (!user) {
    return <p>You must be logged in to access this page</p>
  }
  return (
    <>
      <h1>User MFA Settings</h1>
      {/* Manage SMS MFA */}
      <ManageMfaPhoneNumbers />
      <ManageAvailablePhoneNumbers />
      <Link href="/account/add-phone">Add a new phone number</Link>
      {/* Manage backup codes */}
      {user.twoFactorEnabled && (
        <div>
          <p>
            Generate new backup codes? -{' '}
            <button onClick={() => setShowBackupCodes(true)}>Generate</button>
          </p>
        </div>
      )}
      {showBackupCodes && (
        <>
          <GenerateBackupCodes />
          <button onClick={() => setShowBackupCodes(false)}>Done</button>
        </>
      )}
    </>
  )
}'use client'
import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import { PhoneNumberResource } from '@clerk/types'
export default function Page() {
  const { isLoaded, user } = useUser()
  const [phone, setPhone] = React.useState('')
  const [code, setCode] = React.useState('')
  const [isVerifying, setIsVerifying] = React.useState(false)
  const [successful, setSuccessful] = React.useState(false)
  const [phoneObj, setPhoneObj] = React.useState<PhoneNumberResource | undefined>()
  const createPhoneNumber = useReverification((phone: string) =>
    user?.createPhoneNumber({ phoneNumber: phone }),
  )
  if (!isLoaded) return null
  if (!user) {
    return <p>You must be logged in to access this page</p>
  }
  // Handle addition of the phone number
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    try {
      // Add unverified phone number to user
      const res = await createPhoneNumber(phone)
      // Reload user to get updated User object
      await user.reload()
      // Create a reference to the new phone number to use related methods
      const phoneNumber = user.phoneNumbers.find((a) => a.id === res.id)
      setPhoneObj(phoneNumber)
      // Send the user an SMS with the verification code
      phoneNumber?.prepareVerification()
      // Set to true to display second form
      // and capture the OTP code
      setIsVerifying(true)
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }
  // Handle the submission of the verification form
  const verifyCode = async (e: React.FormEvent) => {
    e.preventDefault()
    try {
      // Verify that the provided code matches the code sent to the user
      const phoneVerifyAttempt = await phoneObj?.attemptVerification({ code })
      if (phoneVerifyAttempt?.verification.status === 'verified') {
        setSuccessful(true)
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(phoneVerifyAttempt, null, 2))
      }
    } catch (err) {
      console.error(JSON.stringify(err, null, 2))
    }
  }
  // Display a success message if the phone number was added successfully
  if (successful) {
    return (
      <>
        <h1>Phone added</h1>
      </>
    )
  }
  // Display the verification form to capture the OTP code
  if (isVerifying) {
    return (
      <>
        <h1>Verify phone</h1>
        <div>
          <form onSubmit={(e) => verifyCode(e)}>
            <div>
              <label htmlFor="code">Enter code</label>
              <input
                onChange={(e) => setCode(e.target.value)}
                id="code"
                name="code"
                type="text"
                value={code}
              />
            </div>
            <div>
              <button type="submit">Verify</button>
            </div>
          </form>
        </div>
      </>
    )
  }
  // Display the initial form to capture the phone number
  return (
    <>
      <h1>Add phone</h1>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <div>
            <label htmlFor="phone">Enter phone number</label>
            <input
              onChange={(e) => setPhone(e.target.value)}
              id="phone"
              name="phone"
              type="phone"
              value={phone}
            />
          </div>
          <div>
            <button type="submit">Continue</button>
          </div>
        </form>
      </div>
    </>
  )
}Feedback
Last updated on