āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/clerk/clerk-docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
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.
<Steps> ## Enable multi-factor authentication[!TIP] To learn how to build a custom flow for managing authenticator app (TOTP) MFA, see the dedicated guide.
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.
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:
Use the following tabs to view the code necessary for each page.
<Include src="_partials/custom-flows/phone-number" /><Tabs items={["Manage MFA page", "Add phone number page"]}> <Tab> ```tsx {{ filename: 'app/account/manage-mfa/page.tsx', collapsible: true }} '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 { isSignedIn, user } = useUser()
if (!isSignedIn) {
// Handle signed out state
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 { isSignedIn, user } = useUser()
const setReservedForSecondFactor = useReverification((phone: PhoneNumberResource) =>
phone.setReservedForSecondFactor({ reserved: true }),
)
const destroyPhone = useReverification((phone: PhoneNumberResource) => phone.destroy())
if (!isSignedIn) {
// Handle signed out state
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/guides/development/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, isSignedIn, user } = useUser()
if (!isLoaded) {
// Handle loading state
return null
}
if (!isSignedIn) {
// Handle signed out state
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>
</>
)}
</>
)
}
```
</Tab>
<Tab>
```tsx {{ filename: 'app/account/add-phone/page.tsx', collapsible: true }}
'use client'
import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import { PhoneNumberResource } from '@clerk/types'
export default function Page() {
const { isLoaded, isSignedIn, 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) {
// Handle loading state
return null
}
if (!isSignedIn) {
// Handle signed out state
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/guides/development/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>
</>
)
}
```
</Tab>
</Tabs>
</Steps>ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā