Build a custom flow for resetting a user's password
The password reset flow works as follows:
- Users can have an email address or phone number, or both. The user enters their email address or phone number and asks for a password reset code.
- Clerk sends an email or SMS to the user, containing a code.
- The user enters the code and a new password.
- Clerk verifies the code, and if successful, updates the user's password and signs them in.
This guide demonstrates how to use Clerk's API to build a custom flow for resetting a user's password. It covers the following scenarios:
Email address
'use client'
import React, { useEffect, useState } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import type { NextPage } from 'next'
import { useRouter } from 'next/navigation'
const ForgotPasswordPage: NextPage = () => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')
  const router = useRouter()
  const clerk = useClerk()
  const { isSignedIn } = useAuth()
  const { isLoaded, signIn, setActive } = useSignIn()
  useEffect(() => {
    if (isSignedIn) {
      router.push('/')
    }
  }, [isSignedIn, router])
  if (!isLoaded) {
    return null
  }
  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_email_code',
        identifier: email,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }
  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_email_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Check for tasks and navigate to custom UI to help users resolve them
                // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
                console.log(session?.currentTask)
                return
              }
              router.push('/')
            },
          })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }
  return (
    <div>
      <h1>Forgot Password?</h1>
      <form onSubmit={!successfulCreation ? create : reset}>
        {!successfulCreation && (
          <>
            <label htmlFor="email">Provide your email address</label>
            <input
              type="email"
              placeholder="e.g john@doe.com"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
            <button>Send password reset code</button>
            {error && <p>{error}</p>}
          </>
        )}
        {successfulCreation && (
          <>
            <label htmlFor="password">Enter your new password</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
            <label htmlFor="code">Enter the password reset code that was sent to your email</label>
            <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />
            <button>Reset</button>
            {error && <p>{error}</p>}
          </>
        )}
        {secondFactor && <p>2FA is required, but this UI does not handle that</p>}
      </form>
    </div>
  )
}
export default ForgotPasswordPageimport SwiftUI
import Clerk
struct ForgotPasswordView: View {
  @Environment(Clerk.self) private var clerk
  @State private var email = ""
  @State private var code = ""
  @State private var newPassword = ""
  @State private var isVerifying = false
  var body: some View {
    switch clerk.client?.signIn?.status {
    case .needsFirstFactor:
      TextField("Enter your code", text: $code)
      Button("Verify") {
        Task { await verify(code: code) }
      }
    case .needsSecondFactor:
      Text("2FA is required, but this UI does not handle that")
    case .needsNewPassword:
      SecureField("New password", text: $newPassword)
      Button("Set new password") {
        Task { await setNewPassword(password: newPassword) }
      }
    default:
      if let session = clerk.session {
        Text("Active Session: \(session.id)")
      } else {
        TextField("Email", text: $email)
        Button("Forgot password?") {
          Task { await createSignIn(email: email) }
        }
      }
    }
  }
}  import android.util.Log
  import androidx.lifecycle.ViewModel
  import androidx.lifecycle.viewModelScope
  import com.clerk.api.Clerk
  import com.clerk.api.network.serialization.longErrorMessageOrNull
  import com.clerk.api.network.serialization.onFailure
  import com.clerk.api.network.serialization.onSuccess
  import com.clerk.api.signin.SignIn
  import com.clerk.api.signin.attemptFirstFactor
  import com.clerk.api.signin.resetPassword
  import kotlinx.coroutines.flow.MutableStateFlow
  import kotlinx.coroutines.flow.asStateFlow
  import kotlinx.coroutines.flow.combine
  import kotlinx.coroutines.flow.launchIn
  import kotlinx.coroutines.launch
  class ForgotPasswordEmailViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState = _uiState.asStateFlow()
    init {
      combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
          _uiState.value =
            when {
              !isInitialized -> UiState.Loading
              user != null -> UiState.Complete
              else -> UiState.SignedOut
            }
        }
        .launchIn(viewModelScope)
    }
    fun createSignIn(email: String) {
      viewModelScope.launch {
        SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordEmailCode(identifier = email))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/guides/development/custom-flows/error-handling
            // for more info on error handling
            Log.e(
              ForgotPasswordEmailViewModel::class.simpleName,
              it.longErrorMessageOrNull,
              it.throwable,
            )
          }
      }
    }
    fun verify(code: String) {
      val inProgressSignIn = Clerk.signIn ?: return
      viewModelScope.launch {
        inProgressSignIn
          .attemptFirstFactor(SignIn.AttemptFirstFactorParams.ResetPasswordEmailCode(code))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/guides/development/custom-flows/error-handling
            // for more info on error handling
            Log.e(
              ForgotPasswordEmailViewModel::class.simpleName,
              it.longErrorMessageOrNull,
              it.throwable,
            )
          }
      }
    }
    fun setNewPassword(password: String) {
      val inProgressSignIn = Clerk.signIn ?: return
      viewModelScope.launch {
        inProgressSignIn
          .resetPassword(SignIn.ResetPasswordParams(password))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/guides/development/custom-flows/error-handling
            // for more info on error handling
            Log.e(
              ForgotPasswordEmailViewModel::class.simpleName,
              it.longErrorMessageOrNull,
              it.throwable,
            )
          }
      }
    }
    fun updateStateFromStatus(status: SignIn.Status) {
      val state =
        when (status) {
          SignIn.Status.COMPLETE -> UiState.Complete
          SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
          SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor
          SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
          else -> {
            UiState.SignedOut
          }
        }
      _uiState.value = state
    }
    sealed interface UiState {
      data object Loading : UiState
      data object SignedOut : UiState
      data object NeedsFirstFactor : UiState
      data object NeedsSecondFactor : UiState
      data object NeedsNewPassword : UiState
      data object Complete : UiState
    }
  }  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk
  class ForgotPasswordEmailActivity : ComponentActivity() {
    val viewModel: ForgotPasswordEmailViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContent {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        ForgotPasswordView(
          state,
          onVerify = viewModel::verify,
          onSetNewPassword = viewModel::setNewPassword,
          onCreateSignIn = viewModel::createSignIn,
        )
      }
    }
  }
  @Composable
  fun ForgotPasswordView(
    state: ForgotPasswordEmailViewModel.UiState,
    onVerify: (String) -> Unit,
    onSetNewPassword: (String) -> Unit,
    onCreateSignIn: (String) -> Unit,
  ) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
      when (state) {
        ForgotPasswordEmailViewModel.UiState.Complete -> {
          Text("Active session: ${Clerk.session?.id}")
        }
        ForgotPasswordEmailViewModel.UiState.NeedsFirstFactor -> {
          InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
        }
        ForgotPasswordEmailViewModel.UiState.NeedsNewPassword -> {
          InputContent(
            placeholder = "Enter your new password",
            buttonText = "Set new password",
            onClick = onSetNewPassword,
            visualTransformation = PasswordVisualTransformation(),
          )
        }
        ForgotPasswordEmailViewModel.UiState.NeedsSecondFactor -> {
          Text("2FA is required but this UI does not handle that")
        }
        ForgotPasswordEmailViewModel.UiState.SignedOut -> {
          InputContent(
            placeholder = "Enter your email address",
            buttonText = "Forgot password?",
            onClick = onCreateSignIn,
          )
        }
        ForgotPasswordEmailViewModel.UiState.Loading -> CircularProgressIndicator()
      }
    }
  }
  @Composable
  fun InputContent(
    placeholder: String,
    buttonText: String,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    onClick: (String) -> Unit,
  ) {
    var value by remember { mutableStateOf("") }
    Column(
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
    ) {
      TextField(
        value = value,
        onValueChange = { value = it },
        visualTransformation = visualTransformation,
        placeholder = { Text(placeholder) },
      )
      Button(onClick = { onClick(value) }) { Text(buttonText) }
    }
  }Prompting users to reset compromised passwords during sign-in
If you have enabled rejection of compromised passwords also on sign-in, then it is possible for the sign-in attempt to be rejected with the form_password_pwned error code.
In this case, you can prompt the user to reset their password using the exact same logic detailed in the previous section.
Phone number
'use client'
import React, { useState, useEffect } from 'react'
import { useClerk, useAuth, useSignIn } from '@clerk/nextjs'
import type { NextPage } from 'next'
import { useRouter } from 'next/navigation'
const ForgotPasswordPage: NextPage = () => {
  const [phoneNumber, setPhoneNumber] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')
  const router = useRouter()
  const { isSignedIn } = useAuth()
  const clerk = useClerk()
  const { isLoaded, signIn, setActive } = useSignIn()
  useEffect(() => {
    if (isSignedIn) {
      router.push('/')
    }
  }, [isSignedIn, router])
  if (!isLoaded) {
    return null
  }
  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_phone_code',
        identifier: phoneNumber,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }
  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_phone_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Check for tasks and navigate to custom UI to help users resolve them
                // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
                console.log(session?.currentTask)
                return
              }
              router.push('/')
            },
          })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }
  return (
    <div>
      <h1>Forgot Password?</h1>
      <form onSubmit={!successfulCreation ? create : reset}>
        {!successfulCreation && (
          <>
            <label htmlFor="phoneNumber">Provide your phone number</label>
            <input
              type="tel"
              placeholder="e.g +1234567890"
              value={phoneNumber}
              onChange={(e) => setPhoneNumber(e.target.value)}
            />
            <button>Send password reset code</button>
            {error && <p>{error}</p>}
          </>
        )}
        {successfulCreation && (
          <>
            <label htmlFor="password">Enter your new password</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
            <label htmlFor="code">
              Enter the password reset code that was sent to your phone number
            </label>
            <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />
            <button>Reset</button>
            {error && <p>{error}</p>}
          </>
        )}
        {secondFactor && <p>2FA is required, but this UI does not handle that</p>}
      </form>
    </div>
  )
}
export default ForgotPasswordPageimport android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.longErrorMessageOrNull
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import com.clerk.api.signin.attemptFirstFactor
import com.clerk.api.signin.resetPassword
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
class ForgotPasswordPhoneViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
  val uiState = _uiState.asStateFlow()
  init {
    combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
        _uiState.value =
          when {
            !isInitialized -> UiState.Loading
            user != null -> UiState.Complete
            else -> UiState.SignedOut
          }
      }
      .launchIn(viewModelScope)
  }
  fun createSignIn(phoneNumber: String) {
    viewModelScope.launch {
      SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordPhoneCode(identifier = phoneNumber))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on error handling
          Log.e(
            ForgotPasswordPhoneViewModel::class.simpleName,
            it.longErrorMessageOrNull,
            it.throwable,
          )
        }
    }
  }
  fun verify(code: String) {
    val inProgressSignIn = Clerk.signIn ?: return
    viewModelScope.launch {
      inProgressSignIn
        .attemptFirstFactor(SignIn.AttemptFirstFactorParams.ResetPasswordPhoneCode(code))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on error handling
          Log.e(
            ForgotPasswordPhoneViewModel::class.simpleName,
            it.longErrorMessageOrNull,
            it.throwable,
          )
        }
    }
  }
  fun setNewPassword(password: String) {
    val inProgressSignIn = Clerk.signIn ?: return
    viewModelScope.launch {
      inProgressSignIn
        .resetPassword(SignIn.ResetPasswordParams(password))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on error handling
          Log.e(
            ForgotPasswordPhoneViewModel::class.simpleName,
            it.longErrorMessageOrNull,
            it.throwable,
          )
        }
    }
  }
  fun updateStateFromStatus(status: SignIn.Status) {
    val state =
      when (status) {
        SignIn.Status.COMPLETE -> UiState.Complete
        SignIn.Status.NEEDS_FIRST_FACTOR -> UiState.NeedsFirstFactor
        SignIn.Status.NEEDS_SECOND_FACTOR -> UiState.NeedsSecondFactor
        SignIn.Status.NEEDS_NEW_PASSWORD -> UiState.NeedsNewPassword
        else -> {
          UiState.SignedOut
        }
      }
    _uiState.value = state
  }
  sealed interface UiState {
    data object Loading : UiState
    data object SignedOut : UiState
    data object NeedsFirstFactor : UiState
    data object NeedsSecondFactor : UiState
    data object NeedsNewPassword : UiState
    data object Complete : UiState
  }
}  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk
  class ForgotPasswordPhoneActivity : ComponentActivity() {
      val viewModel: ForgotPasswordPhoneViewModel by viewModels()
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContent {
              val state by viewModel.uiState.collectAsStateWithLifecycle()
              ForgotPasswordView(
                  state,
                  onVerify = viewModel::verify,
                  onSetNewPassword = viewModel::setNewPassword,
                  onCreateSignIn = viewModel::createSignIn,
              )
          }
      }
  }
  @Composable
  fun ForgotPasswordView(
      state: ForgotPasswordPhoneViewModel.UiState,
      onVerify: (String) -> Unit,
      onSetNewPassword: (String) -> Unit,
      onCreateSignIn: (String) -> Unit,
  ) {
      Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
          when (state) {
              ForgotPasswordPhoneViewModel.UiState.Complete -> {
                  Text("Active session: ${Clerk.session?.id}")
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> {
                  InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> {
                  InputContent(
                      placeholder = "Enter your new password",
                      buttonText = "Set new password",
                      onClick = onSetNewPassword,
                      visualTransformation = PasswordVisualTransformation(),
                  )
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> {
                  Text("2FA is required but this UI does not handle that")
              }
              ForgotPasswordPhoneViewModel.UiState.SignedOut -> {
                  InputContent(
                      placeholder = "Enter your phone number",
                      buttonText = "Forgot password?",
                      onClick = onCreateSignIn,
                  )
              }
              ForgotPasswordPhoneViewModel.UiState.Loading -> CircularProgressIndicator()
          }
      }
  }
  @Composable
  fun InputContent(
      placeholder: String,
      buttonText: String,
      visualTransformation: VisualTransformation = VisualTransformation.None,
      onClick: (String) -> Unit,
  ) {
      var value by remember { mutableStateOf("") }
      Column(
          horizontalAlignment = Alignment.CenterHorizontally,
          verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
      ) {
          TextField(
              value = value,
              onValueChange = { value = it },
              visualTransformation = visualTransformation,
              placeholder = { Text(placeholder) },
          )
          Button(onClick = { onClick(value) }) { Text(buttonText) }
      }
  }Feedback
Last updated on