Create a custom forgot password flow using the Clerk API
Clerk's prebuilt components provide a Forgot Password flow for your users out-of-the-box. However, if you're building a custom user interface, this guide will show you how to use the Clerk API to build a custom Forgot Password flow.
In the following example, the user is asked to provide their email address. After submitting their email, the user is asked to provide a new password and the password reset code that was sent to their email. The user is then signed in with their new password.
'use client'
import React, { useState } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import type { NextPage } from 'next'
import { useRouter } from 'next/navigation'
const ForgotPasswordPage: NextPage = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [code, setCode] = useState('')
const [successfulCreation, setSuccessfulCreation] = useState(false)
const [secondFactor, setSecondFactor] = useState(false)
const [error, setError] = useState('')
const router = useRouter()
const { isSignedIn } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
if (!isLoaded) {
return null
}
// If the user is already signed in,
// redirect them to the home page
if (isSignedIn) {
router.push('/')
}
// Send the password reset code to the user's email
async function create(e: React.FormEvent) {
e.preventDefault()
await signIn
?.create({
strategy: 'reset_password_email_code',
identifier: email,
})
.then((_) => {
setSuccessfulCreation(true)
setError('')
})
.catch((err) => {
console.error('error', err.errors[0].longMessage)
setError(err.errors[0].longMessage)
})
}
// Reset the user's password.
// Upon successful reset, the user will be
// signed in and redirected to the home page
async function reset(e: React.FormEvent) {
e.preventDefault()
await signIn
?.attemptFirstFactor({
strategy: 'reset_password_email_code',
code,
password,
})
.then((result) => {
// Check if 2FA is required
if (result.status === 'needs_second_factor') {
setSecondFactor(true)
setError('')
} else if (result.status === 'complete') {
// Set the active session to
// the newly created session (user is now signed in)
setActive({ session: result.createdSessionId })
setError('')
} else {
console.log(result)
}
})
.catch((err) => {
console.error('error', err.errors[0].longMessage)
setError(err.errors[0].longMessage)
})
}
return (
<div
style={{
margin: 'auto',
maxWidth: '500px',
}}
>
<h1>Forgot Password?</h1>
<form
style={{
display: 'flex',
flexDirection: 'column',
gap: '1em',
}}
onSubmit={!successfulCreation ? create : reset}
>
{!successfulCreation && (
<>
<label htmlFor="email">Provide your email address</label>
<input
type="email"
placeholder="e.g john@doe.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button>Send password reset code</button>
{error && <p>{error}</p>}
</>
)}
{successfulCreation && (
<>
<label htmlFor="password">Enter your new password</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<label htmlFor="password">
Enter the password reset code that was sent to your email
</label>
<input type="text" value={code} onChange={(e) => setCode(e.target.value)} />
<button>Reset</button>
{error && <p>{error}</p>}
</>
)}
{secondFactor && <p>2FA is required, but this UI does not handle that</p>}
</form>
</div>
)
}
export default ForgotPasswordPage
import SwiftUI
import Clerk
struct ForgotPasswordView: View {
@Environment(Clerk.self) private var clerk
@State private var email = ""
@State private var code = ""
@State private var newPassword = ""
@State private var isVerifying = false
var body: some View {
switch clerk.client?.signIn?.status {
case .needsFirstFactor:
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
case .needsSecondFactor:
Text("2FA is required, but this UI does not handle that")
case .needsNewPassword:
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
default:
if let session = clerk.session {
Text("Active Session: \(session.id)")
} else {
TextField("Email", text: $email)
Button("Forgot password?") {
Task { await createSignIn(email: email) }
}
}
}
}
}
extension ForgotPasswordView {
func createSignIn(email: String) async {
do {
// Start the sign in reset password process
try await SignIn.create(strategy: .identifier(email, strategy: "reset_password_email_code"))
} catch {
// See https://clerk.com/docs/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.client?.signIn else { return }
// Verify the code sent to the user's email
try await inProgressSignIn.attemptFirstFactor(for: .resetPasswordEmailCode(code: code))
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
func setNewPassword(password: String) async {
do {
// Access the in progress sign in stored on the client object.
guard let inProgressSignIn = clerk.client?.signIn else { return }
// Reset the user's password.
// Upon successful reset, the user will be signed in
try await inProgressSignIn.resetPassword(.init(password: password, signOutOfOtherSessions: true))
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
}
Prompting users to reset compromised passwords during sign-in
If you have enabled rejection of compromised passwords also on sign-in, then it is possible for the sign-in attempt to be rejected with the form_password_pwned
error code.
In this case, you can prompt the user to reset their password using the exact same logic detailed in the previous section.
Feedback
Last updated on