Skip to main content
Docs

Build a custom flow for resetting a user's password

Warning

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

The password reset flow works as follows:

  1. Users can have an email address or phone number, or both. The user enters their email address or phone number and asks for a password reset code.
  2. Clerk sends an email or SMS to the user, containing a code.
  3. The user enters the code and a new password.
  4. Clerk verifies the code, and if successful, updates the user's password and signs them in.

This guide demonstrates how to use Clerk's API to build a custom flow for resetting a user's password. It covers the following scenarios:

Email address

app/forgot-password.tsx
'use client'
import React, { useEffect, useState } from 'react'
import { useAuth, useSignIn } from '@clerk/nextjs'
import type { NextPage } from 'next'
import { useRouter } from 'next/navigation'

const ForgotPasswordPage: NextPage = () => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  const router = useRouter()
  const clerk = useClerk()
  const { isSignedIn } = useAuth()
  const { isLoaded, signIn, setActive } = useSignIn()

  useEffect(() => {
    if (isSignedIn) {
      router.push('/')
    }
  }, [isSignedIn, router])

  if (!isLoaded) {
    return null
  }

  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_email_code',
        identifier: email,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_email_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Check for tasks and navigate to custom UI to help users resolve them
                // See https://clerk.com/docs/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
ForgotPasswordView.swift
import SwiftUI
import Clerk

struct ForgotPasswordView: View {
  @Environment(Clerk.self) private var clerk
  @State private var email = ""
  @State private var code = ""
  @State private var newPassword = ""
  @State private var isVerifying = false

  var body: some View {
    switch clerk.client?.signIn?.status {
    case .needsFirstFactor:
      TextField("Enter your code", text: $code)
      Button("Verify") {
        Task { await verify(code: code) }
      }

    case .needsSecondFactor:
      Text("2FA is required, but this UI does not handle that")

    case .needsNewPassword:
      SecureField("New password", text: $newPassword)
      Button("Set new password") {
        Task { await setNewPassword(password: newPassword) }
      }

    default:
      if let session = clerk.session {
        Text("Active Session: \(session.id)")
      } else {
        TextField("Email", text: $email)
        Button("Forgot password?") {
          Task { await createSignIn(email: email) }
        }
      }
    }
  }
}
ForgotPasswordEmailViewModel.kt
  import android.util.Log
  import androidx.lifecycle.ViewModel
  import androidx.lifecycle.viewModelScope
  import com.clerk.api.Clerk
  import com.clerk.api.network.serialization.longErrorMessageOrNull
  import com.clerk.api.network.serialization.onFailure
  import com.clerk.api.network.serialization.onSuccess
  import com.clerk.api.signin.SignIn
  import com.clerk.api.signin.attemptFirstFactor
  import com.clerk.api.signin.resetPassword
  import kotlinx.coroutines.flow.MutableStateFlow
  import kotlinx.coroutines.flow.asStateFlow
  import kotlinx.coroutines.flow.combine
  import kotlinx.coroutines.flow.launchIn
  import kotlinx.coroutines.launch

  class ForgotPasswordEmailViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState = _uiState.asStateFlow()

    init {
      combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
          _uiState.value =
            when {
              !isInitialized -> UiState.Loading
              user != null -> UiState.Complete
              else -> UiState.SignedOut
            }
        }
        .launchIn(viewModelScope)
    }

    fun createSignIn(email: String) {
      viewModelScope.launch {
        SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordEmailCode(identifier = email))
          .onSuccess { updateStateFromStatus(it.status) }
          .onFailure {
            // See https://clerk.com/docs/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
    }
  }
ForgotPasswordEmailActivity.kt
  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk

  class ForgotPasswordEmailActivity : ComponentActivity() {
    val viewModel: ForgotPasswordEmailViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContent {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        ForgotPasswordView(
          state,
          onVerify = viewModel::verify,
          onSetNewPassword = viewModel::setNewPassword,
          onCreateSignIn = viewModel::createSignIn,
        )
      }
    }
  }

  @Composable
  fun ForgotPasswordView(
    state: ForgotPasswordEmailViewModel.UiState,
    onVerify: (String) -> Unit,
    onSetNewPassword: (String) -> Unit,
    onCreateSignIn: (String) -> Unit,
  ) {

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
      when (state) {
        ForgotPasswordEmailViewModel.UiState.Complete -> {
          Text("Active session: ${Clerk.session?.id}")
        }

        ForgotPasswordEmailViewModel.UiState.NeedsFirstFactor -> {
          InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
        }
        ForgotPasswordEmailViewModel.UiState.NeedsNewPassword -> {
          InputContent(
            placeholder = "Enter your new password",
            buttonText = "Set new password",
            onClick = onSetNewPassword,
            visualTransformation = PasswordVisualTransformation(),
          )
        }
        ForgotPasswordEmailViewModel.UiState.NeedsSecondFactor -> {
          Text("2FA is required but this UI does not handle that")
        }
        ForgotPasswordEmailViewModel.UiState.SignedOut -> {
          InputContent(
            placeholder = "Enter your email address",
            buttonText = "Forgot password?",
            onClick = onCreateSignIn,
          )
        }

        ForgotPasswordEmailViewModel.UiState.Loading -> CircularProgressIndicator()
      }
    }
  }

  @Composable
  fun InputContent(
    placeholder: String,
    buttonText: String,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    onClick: (String) -> Unit,
  ) {
    var value by remember { mutableStateOf("") }
    Column(
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
    ) {
      TextField(
        value = value,
        onValueChange = { value = it },
        visualTransformation = visualTransformation,
        placeholder = { Text(placeholder) },
      )
      Button(onClick = { onClick(value) }) { Text(buttonText) }
    }
  }

Prompting users to reset compromised passwords during sign-in

If you have enabled rejection of compromised passwords also on sign-in, then it is possible for the sign-in attempt to be rejected with the form_password_pwned error code.

In this case, you can prompt the user to reset their password using the exact same logic detailed in the previous section.

Phone number

app/forgot-password.tsx
'use client'
import React, { useState, useEffect } from 'react'
import { useClerk, useAuth, useSignIn } from '@clerk/nextjs'
import type { NextPage } from 'next'
import { useRouter } from 'next/navigation'

const ForgotPasswordPage: NextPage = () => {
  const [phoneNumber, setPhoneNumber] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [successfulCreation, setSuccessfulCreation] = useState(false)
  const [secondFactor, setSecondFactor] = useState(false)
  const [error, setError] = useState('')

  const router = useRouter()
  const { isSignedIn } = useAuth()
  const clerk = useClerk()
  const { isLoaded, signIn, setActive } = useSignIn()

  useEffect(() => {
    if (isSignedIn) {
      router.push('/')
    }
  }, [isSignedIn, router])

  if (!isLoaded) {
    return null
  }

  // Send the password reset code to the user's email
  async function create(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.create({
        strategy: 'reset_password_phone_code',
        identifier: phoneNumber,
      })
      .then((_) => {
        setSuccessfulCreation(true)
        setError('')
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  // Reset the user's password.
  // Upon successful reset, the user will be
  // signed in and redirected to the home page
  async function reset(e: React.FormEvent) {
    e.preventDefault()
    await signIn
      ?.attemptFirstFactor({
        strategy: 'reset_password_phone_code',
        code,
        password,
      })
      .then((result) => {
        // Check if 2FA is required
        if (result.status === 'needs_second_factor') {
          setSecondFactor(true)
          setError('')
        } else if (result.status === 'complete') {
          // Set the active session to
          // the newly created session (user is now signed in)
          setActive({
            session: result.createdSessionId,
            navigate: async ({ session }) => {
              if (session?.currentTask) {
                // Check for tasks and navigate to custom UI to help users resolve them
                // See https://clerk.com/docs/custom-flows/overview#session-tasks
                console.log(session?.currentTask)
                return
              }

              router.push('/')
            },
          })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  return (
    <div>
      <h1>Forgot Password?</h1>
      <form onSubmit={!successfulCreation ? create : reset}>
        {!successfulCreation && (
          <>
            <label htmlFor="phoneNumber">Provide your phone number</label>
            <input
              type="tel"
              placeholder="e.g +1234567890"
              value={phoneNumber}
              onChange={(e) => setPhoneNumber(e.target.value)}
            />

            <button>Send password reset code</button>
            {error && <p>{error}</p>}
          </>
        )}

        {successfulCreation && (
          <>
            <label htmlFor="password">Enter your new password</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />

            <label htmlFor="code">
              Enter the password reset code that was sent to your phone number
            </label>
            <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />

            <button>Reset</button>
            {error && <p>{error}</p>}
          </>
        )}

        {secondFactor && <p>2FA is required, but this UI does not handle that</p>}
      </form>
    </div>
  )
}

export default ForgotPasswordPage
ForgotPasswordPhoneViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.longErrorMessageOrNull
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import com.clerk.api.signin.attemptFirstFactor
import com.clerk.api.signin.resetPassword
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch

class ForgotPasswordPhoneViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
  val uiState = _uiState.asStateFlow()

  init {
    combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
        _uiState.value =
          when {
            !isInitialized -> UiState.Loading
            user != null -> UiState.Complete
            else -> UiState.SignedOut
          }
      }
      .launchIn(viewModelScope)
  }

  fun createSignIn(phoneNumber: String) {
    viewModelScope.launch {
      SignIn.create(SignIn.CreateParams.Strategy.ResetPasswordPhoneCode(identifier = phoneNumber))
        .onSuccess { updateStateFromStatus(it.status) }
        .onFailure {
          // See https://clerk.com/docs/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
  }
}
ForgotPasswordPhoneActivity.kt
  import android.os.Bundle
  import androidx.activity.ComponentActivity
  import androidx.activity.compose.setContent
  import androidx.activity.viewModels
  import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
  import androidx.compose.foundation.layout.Box
  import androidx.compose.foundation.layout.Column
  import androidx.compose.foundation.layout.fillMaxSize
  import androidx.compose.material3.Button
  import androidx.compose.material3.CircularProgressIndicator
  import androidx.compose.material3.Text
  import androidx.compose.material3.TextField
  import androidx.compose.runtime.Composable
  import androidx.compose.runtime.getValue
  import androidx.compose.runtime.mutableStateOf
  import androidx.compose.runtime.remember
  import androidx.compose.runtime.setValue
  import androidx.compose.ui.Alignment
  import androidx.compose.ui.Modifier
  import androidx.compose.ui.text.input.PasswordVisualTransformation
  import androidx.compose.ui.text.input.VisualTransformation
  import androidx.compose.ui.unit.dp
  import androidx.lifecycle.compose.collectAsStateWithLifecycle
  import com.clerk.api.Clerk

  class ForgotPasswordPhoneActivity : ComponentActivity() {
      val viewModel: ForgotPasswordPhoneViewModel by viewModels()

      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContent {
              val state by viewModel.uiState.collectAsStateWithLifecycle()
              ForgotPasswordView(
                  state,
                  onVerify = viewModel::verify,
                  onSetNewPassword = viewModel::setNewPassword,
                  onCreateSignIn = viewModel::createSignIn,
              )
          }
      }
  }

  @Composable
  fun ForgotPasswordView(
      state: ForgotPasswordPhoneViewModel.UiState,
      onVerify: (String) -> Unit,
      onSetNewPassword: (String) -> Unit,
      onCreateSignIn: (String) -> Unit,
  ) {

      Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
          when (state) {
              ForgotPasswordPhoneViewModel.UiState.Complete -> {
                  Text("Active session: ${Clerk.session?.id}")
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsFirstFactor -> {
                  InputContent(placeholder = "Enter your code", buttonText = "Verify", onClick = onVerify)
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsNewPassword -> {
                  InputContent(
                      placeholder = "Enter your new password",
                      buttonText = "Set new password",
                      onClick = onSetNewPassword,
                      visualTransformation = PasswordVisualTransformation(),
                  )
              }
              ForgotPasswordPhoneViewModel.UiState.NeedsSecondFactor -> {
                  Text("2FA is required but this UI does not handle that")
              }
              ForgotPasswordPhoneViewModel.UiState.SignedOut -> {
                  InputContent(
                      placeholder = "Enter your phone number",
                      buttonText = "Forgot password?",
                      onClick = onCreateSignIn,
                  )
              }

              ForgotPasswordPhoneViewModel.UiState.Loading -> CircularProgressIndicator()
          }
      }
  }

  @Composable
  fun InputContent(
      placeholder: String,
      buttonText: String,
      visualTransformation: VisualTransformation = VisualTransformation.None,
      onClick: (String) -> Unit,
  ) {
      var value by remember { mutableStateOf("") }
      Column(
          horizontalAlignment = Alignment.CenterHorizontally,
          verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically),
      ) {
          TextField(
              value = value,
              onValueChange = { value = it },
              visualTransformation = visualTransformation,
              placeholder = { Text(placeholder) },
          )
          Button(onClick = { onClick(value) }) { Text(buttonText) }
      }
  }

Feedback

What did you think of this content?

Last updated on