Build a custom flow for resetting a user's password
The password reset flow works as follows:
- Users can have an email address or phone number, or both. The user enters their email address or phone number and asks for a password reset code.
- Clerk sends an email or SMS to the user, containing a code.
- The user enters the code and a new password.
- Clerk verifies the code, and if successful, updates the user's password and signs them in.
This guide demonstrates how to use Clerk's API to build a custom flow for resetting a user's password. It covers the following scenarios:
Email address
'use client' import React, { useEffect, 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 clerk = useClerk() const { isSignedIn } = useAuth() const { isLoaded, signIn, setActive } = useSignIn() useEffect(() => { if (isSignedIn) { router.push('/') } }, [isSignedIn, router]) if (!isLoaded) { return null } // 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, navigate: async ({ session }) => { if (session?.currentTask) { // Check for tasks and navigate to custom UI to help users resolve them // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks console.log(session?.currentTask) return } router.push('/') }, }) setError('') } else { console.log(result) } }) .catch((err) => { console.error('error', err.errors[0].longMessage) setError(err.errors[0].longMessage) }) } return ( <div> <h1>Forgot Password?</h1> <form 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="code">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) } } } } } }
import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.clerk.api.Clerk import com.clerk.api.network.serialization.longErrorMessageOrNull import com.clerk.api.network.serialization.onFailure import com.clerk.api.network.serialization.onSuccess import com.clerk.api.signin.SignIn import com.clerk.api.signin.attemptFirstFactor import com.clerk.api.signin.resetPassword import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch class ForgotPasswordEmailViewModel : ViewModel() { private val _uiState = MutableStateFlow<UiState>(UiState.Loading) val uiState = _uiState.asStateFlow() init { combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> _uiState.value = when { !isInitialized -> UiState.Loading user != null -> UiState.Complete else -> UiState.SignedOut } } .launchIn(viewModelScope) } fun createSignIn(email: String) { viewModelScope.launch { SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordEmailCode(identifier = email)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordEmailViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun verify(code: String) { val inProgressSignIn = Clerk.signIn ?: return viewModelScope.launch { inProgressSignIn .attemptFirstFactor(SignIn.AttemptFirstFactorParams.ResetPasswordEmailCode(code)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordEmailViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun setNewPassword(password: String) { val inProgressSignIn = Clerk.signIn ?: return viewModelScope.launch { inProgressSignIn .resetPassword(SignIn.ResetPasswordParams(password)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordEmailViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun updateStateFromStatus(status: SignIn.Status) { val state = when (status) { SignIn.Status.COMPLETE -> UiState.Complete SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword else -> { UiState.SignedOut } } _uiState.value = state } sealed interface UiState { data object Loading : UiState data object SignedOut : UiState data object NeedsFirstFactor : UiState data object NeedsSecondFactor : UiState data object NeedsNewPassword : UiState data object Complete : UiState } }
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.clerk.api.Clerk class ForgotPasswordEmailActivity : ComponentActivity() { val viewModel: ForgotPasswordEmailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.uiState.collectAsStateWithLifecycle() ForgotPasswordView( state, onVerify = viewModel::verify, onSetNewPassword = viewModel::setNewPassword, onCreateSignIn = viewModel::createSignIn, ) } } } @Composable fun ForgotPasswordView( state: ForgotPasswordEmailViewModel.UiState, onVerify: (String) -> Unit, onSetNewPassword: (String) -> Unit, onCreateSignIn: (String) -> Unit, ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { when (state) { ForgotPasswordEmailViewModel.UiState.Complete -> { Text("Active session: ${Clerk.session?.id}") } ForgotPasswordEmailViewModel.UiState.NeedsFirstFactor -> { InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify) } ForgotPasswordEmailViewModel.UiState.NeedsNewPassword -> { InputContent( placeholder = "Enter your new password", buttonText = "Set new password", onClick = onSetNewPassword, visualTransformation = PasswordVisualTransformation(), ) } ForgotPasswordEmailViewModel.UiState.NeedsSecondFactor -> { Text("2FA is required but this UI does not handle that") } ForgotPasswordEmailViewModel.UiState.SignedOut -> { InputContent( placeholder = "Enter your email address", buttonText = "Forgot password?", onClick = onCreateSignIn, ) } ForgotPasswordEmailViewModel.UiState.Loading -> CircularProgressIndicator() } } } @Composable fun InputContent( placeholder: String, buttonText: String, visualTransformation: VisualTransformation = VisualTransformation.None, onClick: (String) -> Unit, ) { var value by remember { mutableStateOf("") } Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), ) { TextField( value = value, onValueChange = { value = it }, visualTransformation = visualTransformation, placeholder = { Text(placeholder) }, ) Button(onClick = { onClick(value) }) { Text(buttonText) } } }
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.
Phone number
'use client' import React, { useState, useEffect } from 'react' import { useClerk, useAuth, useSignIn } from '@clerk/nextjs' import type { NextPage } from 'next' import { useRouter } from 'next/navigation' const ForgotPasswordPage: NextPage = () => { const [phoneNumber, setPhoneNumber] = 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 clerk = useClerk() const { isLoaded, signIn, setActive } = useSignIn() useEffect(() => { if (isSignedIn) { router.push('/') } }, [isSignedIn, router]) if (!isLoaded) { return null } // Send the password reset code to the user's email async function create(e: React.FormEvent) { e.preventDefault() await signIn ?.create({ strategy: 'reset_password_phone_code', identifier: phoneNumber, }) .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_phone_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, navigate: async ({ session }) => { if (session?.currentTask) { // Check for tasks and navigate to custom UI to help users resolve them // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks console.log(session?.currentTask) return } router.push('/') }, }) setError('') } else { console.log(result) } }) .catch((err) => { console.error('error', err.errors[0].longMessage) setError(err.errors[0].longMessage) }) } return ( <div> <h1>Forgot Password?</h1> <form onSubmit={!successfulCreation ? create : reset}> {!successfulCreation && ( <> <label htmlFor="phoneNumber">Provide your phone number</label> <input type="tel" placeholder="e.g +1234567890" value={phoneNumber} onChange={(e) => setPhoneNumber(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="code"> Enter the password reset code that was sent to your phone number </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 android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.clerk.api.Clerk import com.clerk.api.network.serialization.longErrorMessageOrNull import com.clerk.api.network.serialization.onFailure import com.clerk.api.network.serialization.onSuccess import com.clerk.api.signin.SignIn import com.clerk.api.signin.attemptFirstFactor import com.clerk.api.signin.resetPassword import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch class ForgotPasswordPhoneViewModel : ViewModel() { private val _uiState = MutableStateFlow<UiState>(UiState.Loading) val uiState = _uiState.asStateFlow() init { combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> _uiState.value = when { !isInitialized -> UiState.Loading user != null -> UiState.Complete else -> UiState.SignedOut } } .launchIn(viewModelScope) } fun createSignIn(phoneNumber: String) { viewModelScope.launch { SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordPhoneCode(identifier = phoneNumber)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordPhoneViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun verify(code: String) { val inProgressSignIn = Clerk.signIn ?: return viewModelScope.launch { inProgressSignIn .attemptFirstFactor(SignIn.AttemptFirstFactorParams.ResetPasswordPhoneCode(code)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordPhoneViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun setNewPassword(password: String) { val inProgressSignIn = Clerk.signIn ?: return viewModelScope.launch { inProgressSignIn .resetPassword(SignIn.ResetPasswordParams(password)) .onSuccess { updateStateFromStatus(it.status) } .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling Log.e( ForgotPasswordPhoneViewModel::class.simpleName, it.longErrorMessageOrNull, it.throwable, ) } } } fun updateStateFromStatus(status: SignIn.Status) { val state = when (status) { SignIn.Status.COMPLETE -> UiState.Complete SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword else -> { UiState.SignedOut } } _uiState.value = state } sealed interface UiState { data object Loading : UiState data object SignedOut : UiState data object NeedsFirstFactor : UiState data object NeedsSecondFactor : UiState data object NeedsNewPassword : UiState data object Complete : UiState } }
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.clerk.api.Clerk class ForgotPasswordPhoneActivity : ComponentActivity() { val viewModel: ForgotPasswordPhoneViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.uiState.collectAsStateWithLifecycle() ForgotPasswordView( state, onVerify = viewModel::verify, onSetNewPassword = viewModel::setNewPassword, onCreateSignIn = viewModel::createSignIn, ) } } } @Composable fun ForgotPasswordView( state: ForgotPasswordPhoneViewModel.UiState, onVerify: (String) -> Unit, onSetNewPassword: (String) -> Unit, onCreateSignIn: (String) -> Unit, ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { when (state) { ForgotPasswordPhoneViewModel.UiState.Complete -> { Text("Active session: ${Clerk.session?.id}") } ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> { InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify) } ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> { InputContent( placeholder = "Enter your new password", buttonText = "Set new password", onClick = onSetNewPassword, visualTransformation = PasswordVisualTransformation(), ) } ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> { Text("2FA is required but this UI does not handle that") } ForgotPasswordPhoneViewModel.UiState.SignedOut -> { InputContent( placeholder = "Enter your phone number", buttonText = "Forgot password?", onClick = onCreateSignIn, ) } ForgotPasswordPhoneViewModel.UiState.Loading -> CircularProgressIndicator() } } } @Composable fun InputContent( placeholder: String, buttonText: String, visualTransformation: VisualTransformation = VisualTransformation.None, onClick: (String) -> Unit, ) { var value by remember { mutableStateOf("") } Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), ) { TextField( value = value, onValueChange = { value = it }, visualTransformation = visualTransformation, placeholder = { Text(placeholder) }, ) Button(onClick = { onClick(value) }) { Text(buttonText) } } }
Feedback
Last updated on