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/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="password">
Enter the password reset code that was sent to your email
</label>
<input type="code" 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/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/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/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/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/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/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/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