# Build a custom forgot password flow (Legacy)

> This guide is for users who want to build a custom flow. To use a _prebuilt_ UI, use the [Account Portal pages](https://clerk.com/docs/guides/account-portal/overview.md) or [prebuilt components](https://clerk.com/docs/reference/components/overview.md).

> This guide uses the Core 2 `useSignIn()` and `useSignUp()` hooks, which are available in Core 3 SDKs by adding the `/legacy` subpath to the import path. If you're using a Core 2 SDK, remove the `/legacy` subpath.

It's common for a sign-in flow to include a flow for users that forgot their password.

In Clerk, the forgot password flow requires that either **Email** or **Phone** is enabled, as the password reset code can only be delivered to one of these identifiers.

The forgot password 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.
5. If `signOutOfOtherSessions` is `true`, the user is signed out of all other authenticated sessions.

This guide demonstrates how to build a custom user interface for a forgot password flow that you can add to your existing sign-in flow. This guide covers the following scenarios:

- [The user has an email address as an identifier](#email-address)
- [The user has a phone number as an identifier](#phone-number)

> To allow users to update their password **after they've already signed in**, see the [custom flow for updating a user's password](https://clerk.com/docs/guides/development/custom-flows/account-updates/update-password.md).

## Email address

filename: app/forgot-password/page.tsx
```tsx
'use client'

import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [code, setCode] = React.useState('')
  const [successfulCreation, setSuccessfulCreation] = React.useState(false)
  const [error, setError] = React.useState('')

  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) => {
        if (result.status === 'needs_second_factor') {
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
        } 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>}
          </>
        )}
      </form>
    </div>
  )
}
```

## Phone number

filename: app/forgot-password/page.tsx
```tsx
'use client'

import React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { isLoaded, signIn, setActive } = useSignIn()
  const router = useRouter()

  const [phoneNumber, setPhoneNumber] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [code, setCode] = React.useState('')
  const [successfulCreation, setSuccessfulCreation] = React.useState(false)
  const [error, setError] = React.useState('')

  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) => {
        if (result.status === 'needs_second_factor') {
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
        } 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>}
          </>
        )}
      </form>
    </div>
  )
}
```

## Handle compromised passwords

If you have enabled the [**Reject compromised passwords**](https://clerk.com/docs/guides/secure/password-protection-and-rules.md#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.

```json
{
  "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 your instance is older than December 18, 2025, you will need to update your instance to the **Reset password session task** update.

If you have [manually marked a user's password as compromised](https://clerk.com/docs/guides/secure/password-protection-and-rules.md#manually-set-a-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.

```json
{
  "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:

1. 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 OTP or email link), or a phone number (so they can use an SMS OTP). Once they authenticate with another method, their session will enter a `pending` state 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 their session will enter a `pending` state until they reset their password.
2. Handle the `reset-password` session task so the user can reset their password and their session can be updated from `pending` to `active`.

1) #### 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](https://clerk.com/docs/guides/development/custom-flows/authentication/email-password.md) 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.

   filename: app/sign-in/page.tsx

   ```tsx
   'use client'

   import * as React from 'react'
   import { useSignIn } from '@clerk/nextjs'
   import { useRouter } from 'next/navigation'
   import { ClerkAPIError, EmailCodeFactor, SignInFirstFactor } from '@clerk/shared/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&apos;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>
         )}
       </>
     )
   }
   ```
2) #### 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.

   1. First, you need to tell your app where to redirect users when they have pending session tasks.
      The `taskUrls` option 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 `taskUrls` option on the [<ClerkProvider>](https://clerk.com/docs/reference/components/clerk-provider.md) component.

      ```tsx
      <ClerkProvider
        taskUrls={{
          'choose-organization': '/session-tasks/choose-organization',
          'reset-password': '/session-tasks/reset-password',
        }}
      >
        {children}
      </ClerkProvider>
      ```
   2. Now, the user will be redirected to the URL you've set with the `taskUrls` option. This page is where you will add the forgot/reset password code, such as the [reset user's password with an email address](#email-address) example from the previous section.
   3. 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 `pending` user 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 />](https://clerk.com/docs/reference/components/control/redirect-to-tasks.md) component to redirect them to the appropriate task page so they can complete the tasks and move their session to an `active` (signed-in) state. This component will redirect users based on the URL's you've set with the `taskUrls` option.

      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.

      filename: app/layout.tsx

      ```tsx
      import { RedirectToTasks } from '@clerk/nextjs'

      export default function Layout({ children }: { children: React.ReactNode }) {
        return (
          <>
            <RedirectToTasks />
            {children}
          </>
        )
      }
      ```

---

## Sitemap

[Overview of all docs pages](https://clerk.com/docs/llms.txt)
