Build a custom sign-in flow with 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.
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 authentication
This example uses email and password authentication, however, you can modify this approach according to the needs of your application. To use email and password authentication, you first need to enable these authentication strategies in the Clerk Dashboard.
- In the Clerk Dashboard, navigate to the Email, phone, username page.
- Ensure that only Email address is required. If Phone number and Username are enabled, ensure they are not required. Use the settings icon next to each user attribute to check if a setting is required or optional. If you want to require Username, you must collect the username and pass the data to the
create()
method in your custom flow. - In the Authentication strategies section of this page, ensure Password is enabled.
Enable multi-factor authentication
For your users to be able to enable MFA for their account, you need to enable MFA as an authentication strategy in your Clerk application.
- In the Clerk Dashboard, navigate to the Multi-factor page.
- For the purpose of this guide, toggle on both the Authenticator application and Backup codes strategies.
- 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:
- Initiate the sign-in process by collecting the user's email address and password.
- Prepare the first factor verification.
- Attempt to complete the first factor verification.
- Prepare the second factor verification. (This is where MFA comes into play.)
- Attempt to complete the second factor verification.
- If the verification is successful, set the newly created session as the active session.
'use client'
import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export default function SignInForm() {
const { isLoaded, signIn, setActive } = useSignIn()
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const [useBackupCode, setUseBackupCode] = React.useState(false)
const [displayTOTP, setDisplayTOTP] = React.useState(false)
const router = useRouter()
// Handle user submitting email and pass and swapping to TOTP form
const handleFirstStage = (e: React.FormEvent) => {
e.preventDefault()
setDisplayTOTP(true)
}
// Handle the submission of the TOTP of Backup Code submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!isLoaded) return
// Start the sign-in process using the email and password provided
try {
await signIn.create({
identifier: email,
password,
})
// Attempt the TOTP or backup code verification
const signInAttempt = await signIn.attemptSecondFactor({
strategy: useBackupCode ? 'backup_code' : 'totp',
code: code,
})
// If verification was completed, set the session to active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({ session: signInAttempt.createdSessionId })
router.push('/')
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.log(signInAttempt)
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
if (displayTOTP) {
return (
<div>
<h1>Verify your account</h1>
<form onSubmit={(e) => handleSubmit(e)}>
<div>
<label htmlFor="code">Code</label>
<input
onChange={(e) => setCode(e.target.value)}
id="code"
name="code"
type="text"
value={code}
/>
</div>
<div>
<label htmlFor="backupcode">This code is a backup code</label>
<input
onChange={() => setUseBackupCode((prev) => !prev)}
id="backupcode"
name="backupcode"
type="checkbox"
checked={useBackupCode}
/>
</div>
<button type="submit">Verify</button>
</form>
</div>
)
}
return (
<>
<h1>Sign in</h1>
<form onSubmit={(e) => handleFirstStage(e)}>
<div>
<label htmlFor="email">Email</label>
<input
onChange={(e) => setEmail(e.target.value)}
id="email"
name="email"
type="email"
value={email}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
onChange={(e) => setPassword(e.target.value)}
id="password"
name="password"
type="password"
value={password}
/>
</div>
<button type="submit">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
Last updated on