Skip to main content
Docs

Build a custom flow for resetting a user's password

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, use the Account Portal pages or prebuilt components.

The password reset flow works as follows:

  1. 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.
  2. Clerk sends an email or SMS to the user, containing a code.
  3. The user enters the code and a new password.
  4. 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

app/forgot-password.tsx
'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
ForgotPasswordView.swift
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) }  }  }  }  } }
ForgotPasswordEmailViewModel.kt
 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  }  }
ForgotPasswordEmailActivity.kt
 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

app/forgot-password.tsx
'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
ForgotPasswordPhoneViewModel.kt
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  } }
ForgotPasswordPhoneActivity.kt
 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