Docs

Create a custom Forgot Password flow using the Clerk API

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.

Clerk's prebuilt components provide a Forgot Password flow for your users out-of-the-box. However, if you're building a custom user interface, this guide will show you how to use the Clerk API to build a custom Forgot Password flow.

In the following example, the user is asked to provide their email address. After submitting their email, the user is asked to provide a new password and the password reset code that was sent to their email. The user is then signed in with their new password.

Note

This example's user interface does not handle two-factor authentication (2FA). If it detects that 2FA is needed for the account trying to reset the password, it will display a message to the user that says "2FA is required, but this UI does not handle that".

For the sake of this guide, this example is written for Next.js App Router but it is supported by any React meta framework, such as Remix.

app/forgot-password.tsx
'use client'
import React, { 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 { isSignedIn } = useAuth()
  const { isLoaded, signIn, setActive } = useSignIn()

  if (!isLoaded) {
    return null
  }

  // If the user is already signed in,
  // redirect them to the home page
  if (isSignedIn) {
    router.push('/')
  }

  // 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 })
          setError('')
        } else {
          console.log(result)
        }
      })
      .catch((err) => {
        console.error('error', err.errors[0].longMessage)
        setError(err.errors[0].longMessage)
      })
  }

  return (
    <div
      style={{
        margin: 'auto',
        maxWidth: '500px',
      }}
    >
      <h1>Forgot Password?</h1>
      <form
        style={{
          display: 'flex',
          flexDirection: 'column',
          gap: '1em',
        }}
        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="text" value={code} onChange={(e) => setCode(e.target.value)} />

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

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

export default ForgotPasswordPage
ForgotPasswordView.swift
import SwiftUI
import ClerkSDK

struct ForgotPasswordView: View {
  @ObservedObject private var clerk = Clerk.shared
  @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:
      if isVerifying {
        TextField("Enter your code", text: $code)
        Button("Verify") {
          Task { await verify(code: code) }
        }
      } else {
        Button("Forgot password?") {
          Task { await sendResetCode() }
        }
      }

    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("Continue") {
          Task { await createSignIn(email: email) }
        }
      }
    }
  }
}

extension ForgotPasswordView {

  func createSignIn(email: String) async {
    do {
      // Start the sign in process
      try await SignIn.create(strategy: .identifier(email))
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }

  func sendResetCode() async {
    do {
      // Access the in progress sign in stored on the client object.
      guard let inProgressSignIn = clerk.client?.signIn else { return }

      // Send the password reset code to the user's email
      try await inProgressSignIn.prepareFirstFactor(for: .resetPasswordEmailCode)

      // Set isVerifying to true to capture the OTP code.
      isVerifying = true
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }

  func verify(code: String) async {
    do {
      // Access the in progress sign in stored on the client object.
      guard let inProgressSignIn = clerk.client?.signIn else { return }

      // Verify the code sent to the user's email
      try await inProgressSignIn.attemptFirstFactor(for: .resetPasswordEmailCode(code: code))
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }

  func setNewPassword(password: String) async {
    do {
      // Access the in progress sign in stored on the client object.
      guard let inProgressSignIn = clerk.client?.signIn else { return }

      // Reset the user's password.
      // Upon successful reset, the user will be signed in
      try await inProgressSignIn.resetPassword(.init(password: password, signOutOfOtherSessions: true))
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }
}

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.

Feedback

What did you think of this content?

Last updated on