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:
Reset user's password with an email address
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
'use client'
import React, { useEffect, useState } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export default function Page() {
const { isSignedIn } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
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('')
// Redirect signed-in users to home
useEffect(() => {
if (isSignedIn) router.push('/')
}, [isSignedIn, router])
if (!isLoaded) return <p>Loading...</p>
// 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) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/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>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useUser } from '@clerk/clerk-expo'
import { Redirect, useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function Page() {
const { isSignedIn } = useUser()
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
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('')
// Redirect signed-in users to home
if (isSignedIn) {
return <Redirect href="/" />
}
if (!isLoaded) {
return (
<ThemedView style={styles.container}>
<ThemedText>Loading...</ThemedText>
</ThemedView>
)
}
// Send the password reset code to the user's email
async function create() {
try {
await signIn?.create({
strategy: 'reset_password_email_code',
identifier: email,
})
setSuccessfulCreation(true)
setError('')
} catch (err: any) {
console.error('error', err.errors?.[0]?.longMessage)
setError(err.errors?.[0]?.longMessage || 'An error occurred')
}
}
// Reset the user's password.
// Upon successful reset, the user will be
// signed in and redirected to the home page
async function reset() {
try {
const result = await signIn?.attemptFirstFactor({
strategy: 'reset_password_email_code',
code,
password,
})
if (!result) return
// 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)
await setActive?.({
session: result.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.replace('/')
},
})
setError('')
} else {
console.log(result)
}
} catch (err: any) {
console.error('error', err.errors?.[0]?.longMessage)
setError(err.errors?.[0]?.longMessage || 'An error occurred')
}
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
{!successfulCreation && (
<>
<ThemedText style={styles.label}>Provide your email address</ThemedText>
<TextInput
style={styles.input}
placeholder="e.g john@doe.com"
placeholderTextColor="#666666"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<Pressable
style={({ pressed }) => [
styles.button,
!email && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={create}
disabled={!email}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
)}
</>
)}
{successfulCreation && !secondFactor && (
<>
<ThemedText style={styles.description}>
A password reset code has been sent to your email
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
placeholder="Enter new password"
placeholderTextColor="#666666"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
/>
<ThemedText style={styles.label}>Enter the password reset code</ThemedText>
<TextInput
style={styles.input}
placeholder="Enter code"
placeholderTextColor="#666666"
value={code}
onChangeText={setCode}
keyboardType="numeric"
/>
<Pressable
style={({ pressed }) => [
styles.button,
(!password || !code) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={reset}
disabled={!password || !code}
>
<ThemedText style={styles.buttonText}>Reset Password</ThemedText>
</Pressable>
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
)}
</>
)}
{secondFactor && (
<View style={styles.warningContainer}>
<ThemedText style={styles.warningText}>
2FA is required, but this UI does not handle that yet
</ThemedText>
</View>
)}
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
description: {
fontSize: 14,
marginBottom: 16,
opacity: 0.8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
errorContainer: {
padding: 12,
backgroundColor: '#ffebee',
borderRadius: 8,
marginTop: 8,
},
errorText: {
color: '#c62828',
fontWeight: '500',
},
warningContainer: {
padding: 12,
backgroundColor: '#fff3e0',
borderRadius: 8,
marginTop: 8,
},
warningText: {
color: '#e65100',
fontWeight: '500',
},
}) import SwiftUI
import ClerkKit
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.auth.currentSignIn?.status {
case .needsFirstFactor:
// Verify the reset code.
TextField("Enter your code", text: $code)
Button("Verify") {
Task { await verify(code: code) }
}
case .needsSecondFactor:
// Handle any additional verification requirements.
Text("2FA is required, but this UI does not handle that")
case .needsNewPassword:
// Set the new password after verification.
SecureField("New password", text: $newPassword)
Button("Set new password") {
Task { await setNewPassword(password: newPassword) }
}
default:
if let user = clerk.user {
// Show the current user after a successful reset.
Text("Signed in as: \(user.id)")
} else {
// Start the reset flow by creating a sign-in attempt.
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.errorMessage
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.errorMessage,
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.errorMessage,
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.errorMessage,
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.activeSession?.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) }
}
}Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
'use client'
import React, { useState, useEffect } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export default function Page() {
const { isSignedIn } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
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('')
// Redirect signed-in users to home
useEffect(() => {
if (isSignedIn) router.push('/')
}, [isSignedIn, router])
if (!isLoaded) return <p>Loading...</p>
// 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) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/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>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useUser } from '@clerk/clerk-expo'
import { Redirect, useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function ForgotPasswordPage() {
const { isSignedIn } = useUser()
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
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('')
// Redirect signed-in users to home
if (isSignedIn) {
return <Redirect href="/" />
}
if (!isLoaded) {
return (
<ThemedView style={styles.container}>
<ThemedText>Loading...</ThemedText>
</ThemedView>
)
}
// Send the password reset code to the user's phone
async function create() {
try {
await signIn?.create({
strategy: 'reset_password_phone_code',
identifier: phoneNumber,
})
setSuccessfulCreation(true)
setError('')
} catch (err: any) {
console.error('error', err.errors?.[0]?.longMessage)
setError(err.errors?.[0]?.longMessage || 'An error occurred')
}
}
// Reset the user's password.
// Upon successful reset, the user will be
// signed in and redirected to the home page
async function reset() {
try {
const result = await signIn?.attemptFirstFactor({
strategy: 'reset_password_phone_code',
code,
password,
})
if (!result) return
// 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)
await setActive?.({
session: result.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.replace('/')
},
})
setError('')
} else {
console.log(result)
}
} catch (err: any) {
console.error('error', err.errors?.[0]?.longMessage)
setError(err.errors?.[0]?.longMessage || 'An error occurred')
}
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Forgot Password?
</ThemedText>
{!successfulCreation && (
<>
<ThemedText style={styles.label}>Provide your phone number</ThemedText>
<TextInput
style={styles.input}
placeholder="e.g +1234567890"
placeholderTextColor="#666666"
value={phoneNumber}
onChangeText={setPhoneNumber}
keyboardType="phone-pad"
/>
<Pressable
style={({ pressed }) => [
styles.button,
!phoneNumber && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={create}
disabled={!phoneNumber}
>
<ThemedText style={styles.buttonText}>Send password reset code</ThemedText>
</Pressable>
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
)}
</>
)}
{successfulCreation && !secondFactor && (
<>
<ThemedText style={styles.description}>
A password reset code has been sent to your phone
</ThemedText>
<ThemedText style={styles.label}>Enter your new password</ThemedText>
<TextInput
style={styles.input}
placeholder="Enter new password"
placeholderTextColor="#666666"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
/>
<ThemedText style={styles.label}>Enter the password reset code</ThemedText>
<TextInput
style={styles.input}
placeholder="Enter code"
placeholderTextColor="#666666"
value={code}
onChangeText={setCode}
keyboardType="numeric"
/>
<Pressable
style={({ pressed }) => [
styles.button,
(!password || !code) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={reset}
disabled={!password || !code}
>
<ThemedText style={styles.buttonText}>Reset Password</ThemedText>
</Pressable>
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
)}
</>
)}
{secondFactor && (
<View style={styles.warningContainer}>
<ThemedText style={styles.warningText}>
2FA is required, but this UI does not handle that yet
</ThemedText>
</View>
)}
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
description: {
fontSize: 14,
marginBottom: 16,
opacity: 0.8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
errorContainer: {
padding: 12,
backgroundColor: '#ffebee',
borderRadius: 8,
marginTop: 8,
},
errorText: {
color: '#c62828',
fontWeight: '500',
},
warningContainer: {
padding: 12,
backgroundColor: '#fff3e0',
borderRadius: 8,
marginTop: 8,
},
warningText: {
color: '#e65100',
fontWeight: '500',
},
})import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.errorMessage
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.errorMessage,
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.errorMessage,
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.errorMessage,
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.activeSession?.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) }
}
}Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.
Handle compromised passwords
If you have enabled the Reject compromised passwords setting, sign-up/sign-in and password update attempts will be rejected with an error if the password is compromised.
User is trying to set a compromised password
If the user is trying to set a password that is compromised, the attempt will receive an HTTP status of 422 (Unprocessable Entity) and the form_password_pwned error code.
{
"errors": [
{
"shortMessage": "Password has been found in an online data breach. For account safety, please <action>.",
"code": "form_password_pwned",
"meta": {
"name": "param"
}
}
]
}In this case, the user just needs to be prompted to use a different password. For example, you can add text to the form with the error's message so that the user can try again.
User's password has been marked as compromised
If you have manually marked a user's password as compromised, and the user tries authenticating with it, the sign-up/sign-in attempt will receive an HTTP status of 422 (Unprocessable Entity) and the form_password_compromised error code.
{
"errors": [
{
"long_message": "Your password may be compromised. To protect your account, please continue with an alternative sign-in method. You will be required to reset your password after signing in.",
"code": "form_password_compromised",
"meta": {
"name": "param"
}
}
]
}The user will not be able to authenticate with their compromised password, so you should:
- Update your sign-up or sign-in flow to prompt them to authenticate with another method, such as an email address (so they can use email or email link), or a phone number (so they can use an SMS ). Once they authenticate with another method, until they reset their password. If they do not have any other identification methods, e.g if they only have username and password, they will be authenticated but until they reset their password.
- Handle the
reset-passwordsession task so the user can reset their password and their session can be updated frompendingtoactive.
Update your sign-up/sign-in flow
To update your sign-up/sign-in flow, you can check for the error code and then prompt the user to authenticate with another method. This example uses the email/password custom flow but adds code that prompts the user to authenticate with an email code when their password is compromised. You can use the same approach with other custom flows.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
'use client'
import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import { ClerkAPIError, EmailCodeFactor, SignInFirstFactor } from '@clerk/types'
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
const SignInWithEmailCode = () => {
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
const [verifying, setVerifying] = React.useState(false)
const [email, setEmail] = React.useState('')
const [code, setCode] = React.useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!isLoaded && !signIn) return null
try {
// Start the sign-in process using the email code method
const { supportedFirstFactors } = await signIn.create({
identifier: email,
})
// Filter the returned array to find the 'email_code' entry
const isEmailCodeFactor = (factor: SignInFirstFactor): factor is EmailCodeFactor => {
return factor.strategy === 'email_code'
}
const emailCodeFactor = supportedFirstFactors?.find(isEmailCodeFactor)
if (emailCodeFactor) {
// Grab the emailAddressId
const { emailAddressId } = emailCodeFactor
// Send the OTP code to the user
await signIn.prepareFirstFactor({
strategy: 'email_code',
emailAddressId,
})
// Set verifying to true to display second form
// and capture the OTP code
setVerifying(true)
}
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
async function handleVerification(e: React.FormEvent) {
e.preventDefault()
if (!isLoaded && !signIn) return null
try {
// Use the code provided by the user and attempt verification
const signInAttempt = await signIn.attemptFirstFactor({
strategy: 'email_code',
code,
})
// If verification was completed, set the session to active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({
session: signInAttempt.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.push('/')
},
})
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(signInAttempt)
}
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
if (verifying) {
return (
<>
<h1>Verify your email address</h1>
<form onSubmit={handleVerification}>
<label htmlFor="code">Enter your email verification code</label>
<input value={code} id="code" name="code" onChange={(e) => setCode(e.target.value)} />
<button type="submit">Verify</button>
</form>
</>
)
}
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Enter email address</label>
<input
value={email}
id="email"
name="email"
type="email"
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Continue</button>
</form>
</>
)
}
export default function SignInForm() {
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState<ClerkAPIError[]>()
// Handle the submission of the sign-in form
const handleSignInWithPassword = async (e: React.FormEvent) => {
e.preventDefault()
// Clear any errors that may have occurred during previous form submission
setErrors(undefined)
if (!isLoaded) {
return
}
// Start the sign-in process using the email and password provided
try {
const signInAttempt = await signIn.create({
identifier: email,
password,
})
// If sign-in process is complete, set the created session as active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({
session: signInAttempt.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.push('/')
},
})
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err) {
if (isClerkAPIResponseError(err)) setErrors(err.errors)
console.error(JSON.stringify(err, null, 2))
}
}
if (errors && errors[0].code === 'form_password_compromised') {
return (
<>
<h1>Sign in</h1>
<p>
Your password appears to have been compromised or it's no longer trusted and cannot
be used. Please use email code to continue.
</p>
<SignInWithEmailCode />
</>
)
}
// Display a form to capture the user's email and password
return (
<>
<h1>Sign in</h1>
<form onSubmit={(e) => handleSignInWithPassword(e)}>
<div>
<label htmlFor="email">Enter email address</label>
<input
onChange={(e) => setEmail(e.target.value)}
id="email"
name="email"
type="email"
value={email}
/>
</div>
<div>
<label htmlFor="password">Enter password</label>
<input
onChange={(e) => setPassword(e.target.value)}
id="password"
name="password"
type="password"
value={password}
/>
</div>
<button type="submit">Sign in</button>
</form>
{errors && (
<ul>
{errors.map((el, index) => (
<li key={index}>{el.longMessage}</li>
))}
</ul>
)}
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { isClerkAPIResponseError, useSignIn } from '@clerk/clerk-expo'
import { ClerkAPIError, EmailCodeFactor, SignInFirstFactor } from '@clerk/types'
import { Link, useRouter } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
const SignInWithEmailCode = () => {
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
const [verifying, setVerifying] = React.useState(false)
const [email, setEmail] = React.useState('')
const [code, setCode] = React.useState('')
async function handleSubmit() {
if (!isLoaded && !signIn) return
try {
// Start the sign-in process using the email code method
const { supportedFirstFactors } = await signIn.create({
identifier: email,
})
// Filter the returned array to find the 'email_code' entry
const isEmailCodeFactor = (factor: SignInFirstFactor): factor is EmailCodeFactor => {
return factor.strategy === 'email_code'
}
const emailCodeFactor = supportedFirstFactors?.find(isEmailCodeFactor)
if (emailCodeFactor) {
// Grab the emailAddressId
const { emailAddressId } = emailCodeFactor
// Send the OTP code to the user
await signIn.prepareFirstFactor({
strategy: 'email_code',
emailAddressId,
})
// Set verifying to true to display second form
// and capture the OTP code
setVerifying(true)
}
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
async function handleVerification() {
if (!isLoaded && !signIn) return
try {
// Use the code provided by the user and attempt verification
const signInAttempt = await signIn.attemptFirstFactor({
strategy: 'email_code',
code,
})
// If verification was completed, set the session to active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({
session: signInAttempt.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.replace('/')
},
})
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(signInAttempt)
}
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error('Error:', JSON.stringify(err, null, 2))
}
}
if (verifying) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify your email address
</ThemedText>
<ThemedText style={styles.label}>Enter your email verification code</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter code"
placeholderTextColor="#666666"
onChangeText={setCode}
keyboardType="numeric"
/>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={handleVerification}
>
<ThemedText style={styles.buttonText}>Verify</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText style={styles.label}>Enter email address</ThemedText>
<TextInput
style={styles.input}
value={email}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<Pressable
style={({ pressed }) => [
styles.button,
!email && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSubmit}
disabled={!email}
>
<ThemedText style={styles.buttonText}>Continue</ThemedText>
</Pressable>
</ThemedView>
)
}
export default function SignInForm() {
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState<ClerkAPIError[]>()
// Handle the submission of the sign-in form
const handleSignInWithPassword = async () => {
// Clear any errors that may have occurred during previous form submission
setErrors(undefined)
if (!isLoaded) {
return
}
// Start the sign-in process using the email and password provided
try {
const signInAttempt = await signIn.create({
identifier: email,
password,
})
// If sign-in process is complete, set the created session as active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({
session: signInAttempt.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
router.replace('/')
},
})
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err) {
if (isClerkAPIResponseError(err)) setErrors(err.errors)
console.error(JSON.stringify(err, null, 2))
}
}
if (errors && errors[0].code === 'form_password_compromised') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign in
</ThemedText>
<View style={styles.warningContainer}>
<ThemedText style={styles.warningText}>
Your password appears to have been compromised or it's no longer trusted and cannot be
used. Please use email code to continue.
</ThemedText>
</View>
<SignInWithEmailCode />
</ThemedView>
)
}
// Display a form to capture the user's email and password
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign in
</ThemedText>
<ThemedText style={styles.label}>Email address</ThemedText>
<TextInput
style={styles.input}
value={email}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<ThemedText style={styles.label}>Password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
onChangeText={setPassword}
secureTextEntry={true}
/>
<Pressable
style={({ pressed }) => [
styles.button,
(!email || !password) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSignInWithPassword}
disabled={!email || !password}
>
<ThemedText style={styles.buttonText}>Sign in</ThemedText>
</Pressable>
{errors && (
<View style={styles.errorContainer}>
{errors.map((el, index) => (
<ThemedText key={index} style={styles.errorText}>
• {el.longMessage}
</ThemedText>
))}
</View>
)}
<View style={styles.linkContainer}>
<ThemedText>Don't have an account? </ThemedText>
<Link href="/sign-up">
<ThemedText type="link">Sign up</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
errorContainer: {
padding: 12,
backgroundColor: '#ffebee',
borderRadius: 8,
marginTop: 8,
gap: 4,
},
errorText: {
color: '#c62828',
fontWeight: '500',
fontSize: 14,
},
warningContainer: {
padding: 12,
backgroundColor: '#fff3e0',
borderRadius: 8,
marginBottom: 16,
},
warningText: {
color: '#e65100',
fontWeight: '500',
fontSize: 14,
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
})Handle the reset-password session task
Once a user has authenticated, their password is still considered compromised. The reset-password session task will cause the user's session to be in a pending state until they reset their password.
-
First, you need to tell your app where to redirect users when they have pending session tasks. The
taskUrlsoption allows you to specify custom URL paths where users are redirected after sign-up or sign-in when specific session tasks need to be completed.Configure the
taskUrlsoption on the <ClerkProvider> component.<ClerkProvider taskUrls={{ 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }} > {children} </ClerkProvider>Configure the
taskUrlsoption on the clerk() integration.astro.config.mjs import { defineConfig } from 'astro/config' import node from '@astrojs/node' import clerk from '@clerk/astro' export default defineConfig({ integrations: [ clerk({ taskUrls: { 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }, }), ], adapter: node({ mode: 'standalone' }), output: 'server', })Configure the
taskUrlsoption on theclerk.load()method.main.ts import { Clerk } from '@clerk/clerk-js' const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY const clerk = new Clerk(clerkPubKey) await clerk.load({ taskUrls: { 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }, })Configure the
taskUrlsoption on the clerkPlugin() integration.src /main.ts import { createApp } from 'vue' import './styles.css' import App from './App.vue' import { clerkPlugin } from '@clerk/vue' const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY if (!PUBLISHABLE_KEY) { throw new Error('Add your Clerk publishable key to the .env.local file') } const app = createApp(App) app.use(clerkPlugin, { publishableKey: PUBLISHABLE_KEY, taskUrls: { 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }, }) app.mount('#app')Configure the
taskUrlsoption on the defineNuxtConfig() integration.nuxt.config.ts export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: true }, modules: ['@clerk/nuxt'], clerk: { taskUrls: { 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }, }, })Configure the
taskUrlsoption on the clerkPlugin() integration.src /main.ts import Fastify from 'fastify' import { clerkPlugin } from '@clerk/fastify' const fastify = Fastify({ logger: true }) fastify.register(clerkPlugin, { taskUrls: { 'choose-organization': '/session-tasks/choose-organization', 'reset-password': '/session-tasks/reset-password', }, }) -
Now, the user will be redirected to the URL you've set with the
taskUrlsoption. This page is where you will add the forgot/reset password code, such as the reset user's password with an email address example from the previous section. -
What if your user exits the authentication or session task flow before completing their tasks and doesn't know how to get to the appropriate page to complete their session tasks? What if your user is navigating through your app as a
pendinguser and can't figure out why they can't access certain content? If a user's authentication or session task flow is interrupted and they aren't able to complete the tasks, you can use the <RedirectToTasks /> component to redirect them to the appropriate task page so they can complete the tasks and move their session to anactive(signed-in) state. This component will redirect users based on the URL's you've set with thetaskUrlsoption.In the following example, the
<RedirectToTasks />component is used to protect a page. Users can't access this page until they complete their pending session tasks. You can also wrap your entire application in the<RedirectToTasks />component, or place it in your application's layout file, so that users can't access any of your app until they complete their pending session tasks.In the following example, the
<RedirectToTasks />component is used in the app's layout file so that users can't access any of the app until they complete their pending session tasks. However, you can also use the<RedirectToTasks />component to protect a single page or route group.app /layout.tsx import { RedirectToTasks } from '@clerk/nextjs' export default function Layout({ children }: { children: React.ReactNode }) { return ( <> <RedirectToTasks /> {children} </> ) }pages /index.tsx import { RedirectToTasks } from '@clerk/clerk-react' export default function Page() { return <RedirectToTasks /> }app /routes /home.tsx import { RedirectToTasks } from '@clerk/react-router' export default function Home() { return <RedirectToTasks /> }src /routes /home.tsx import { RedirectToTasks } from '@clerk/chrome-extension' export default function Home() { return <RedirectToTasks /> }app /routes /index.tsx import { RedirectToTasks } from '@clerk/tanstack-react-start' import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/')({ component: Home, }) function Home() { return <RedirectToTasks /> }App.vue <script setup lang="ts"> import { RedirectToTasks } from '@clerk/vue' </script> <template> <RedirectToTasks /> </template>App.vue <script setup lang="ts"> // Components are automatically imported </script> <template> <RedirectToTasks /> </template>This component is not available for your SDK. Please choose a different SDK.
Feedback
Last updated on