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
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.
- In the Clerk Dashboard, navigate to the User & authentication page.
- Enable Sign-in with email.
- 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.
- 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 { 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>
</>
)
}
import SwiftUI
import Clerk
struct MFASignInView: View {
@State private var email = ""
@State private var password = ""
@State private var code = ""
@State private var displayTOTP = false
var body: some View {
if displayTOTP {
TextField("Code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
} else {
TextField("Email", text: $email)
SecureField("Password", text: $password)
Button("Next") {
Task { await submit(email: email, password: password) }
}
}
}
}
extension MFASignInView {
func submit(email: String, password: String) async {
do {
// Start the sign-in process.
let signIn = try await SignIn.create(strategy: .identifier(email, password: password))
switch signIn.status {
case .needsSecondFactor:
// Handle user submitting email and password and swapping to TOTP form.
displayTOTP = true
default:
// If the status is not needsSecondFactor, check why. User may need to
// complete different steps.
dump(signIn.status)
}
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
func verify(code: String) async {
do {
// Access the in progress sign in stored on the client object.
guard let inProgressSignIn = Clerk.shared.client?.signIn else { return }
// Attempt the TOTP or backup code verification.
let signIn = try await inProgressSignIn.attemptSecondFactor(strategy: .totp(code: code))
switch signIn.status {
case .complete:
// If sign-in process is complete, navigate the user as needed.
dump(Clerk.shared.session)
default:
// If the status is not complete, check why. User may need to
// complete further steps.
dump(signIn.status)
}
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
}
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