From 3a681364254cfe436750d9102646ff45d3b7411f Mon Sep 17 00:00:00 2001
From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com>
Date: Tue, 25 Feb 2025 17:51:53 -0500
Subject: [PATCH] update forgot password flow (#2064)
---
docs/custom-flows/forgot-password.mdx | 536 +++++++++++++++----------
docs/references/javascript/sign-in.mdx | 4 +-
2 files changed, 334 insertions(+), 206 deletions(-)
diff --git a/docs/custom-flows/forgot-password.mdx b/docs/custom-flows/forgot-password.mdx
index 09deb8469d..5d4f2857ad 100644
--- a/docs/custom-flows/forgot-password.mdx
+++ b/docs/custom-flows/forgot-password.mdx
@@ -1,241 +1,369 @@
---
-title: Create a custom forgot password flow using the Clerk API
-description: Create a custom forgot password flow for your users using the lower level methods provided by the ClerkJS SDK.
+title: Build a custom flow for resetting a user's password
+description: Learn how to build a custom flow using Clerk's API to reset a user's password.
---
-Clerk's [prebuilt components](/docs/components/overview) 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.
+The password reset flow works as follows:
-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.
+1. Users can have an email address or phone number, or both, as an [identifier](/docs/authentication/configuration/sign-up-sign-in-options#identifiers). The user enters their email address or phone number and asks for a password reset code.
+1. Clerk sends an email or SMS to the user, containing a code.
+1. The user enters the code and a new password.
+1. Clerk verifies the code, and if successful, updates the user's password and signs them in.
-> [!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".
+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:
-{/* TODO: Add JavaScript example. */}
+- [The user has an email address as an identifier](#email-address)
+- [The user has a phone number as an identifier](#phone-number)
-
- ```tsx {{ filename: 'app/forgot-password.tsx', collapsible: true }}
- 'use client'
- import React, { useState } from 'react'
- import { useAuth, useSignIn } from '@clerk/nextjs'
- import type { NextPage } from 'next'
- import { useRouter } from 'next/navigation'
+## Email address
- 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('')
+{/* TODO: Add vanilla JS example. */}
- const router = useRouter()
- const { isSignedIn } = useAuth()
- const { isLoaded, signIn, setActive } = useSignIn()
+
+
+ ```tsx {{ filename: 'app/forgot-password.tsx', collapsible: true }}
+ 'use client'
+ import React, { useEffect, useState } from 'react'
+ import { useAuth, useSignIn } from '@clerk/nextjs'
+ import type { NextPage } from 'next'
+ import { useRouter } from 'next/navigation'
- if (!isLoaded) {
- return null
- }
+ 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('')
- // If the user is already signed in,
- // redirect them to the home page
- if (isSignedIn) {
- router.push('/')
- }
+ const router = useRouter()
+ const { isSignedIn } = useAuth()
+ const { isLoaded, signIn, setActive } = useSignIn()
- // 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)
- })
- }
+ useEffect(() => {
+ if (isSignedIn) {
+ router.push('/')
+ }
+ }, [isSignedIn, router])
- // 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 })
+ 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('')
- } else {
- console.log(result)
- }
- })
- .catch((err) => {
- console.error('error', err.errors[0].longMessage)
- setError(err.errors[0].longMessage)
- })
+ })
+ .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 (
+
+
Forgot Password?
+
+
+ )
}
- return (
-
-
Forgot Password?
-
-
- )
- }
-
- export default ForgotPasswordPage
- ```
-
- ```swift {{ filename: 'ForgotPasswordView.swift', collapsible: true }}
- 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) }
- }
+ export default ForgotPasswordPage
+ ```
+
+
+
+ ```swift {{ filename: 'ForgotPasswordView.swift', collapsible: true }}
+ 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 .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) }
- }
+ 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) }
+ default:
+ if let session = clerk.session {
+ Text("Active Session: \(session.id)")
+ } else {
+ TextField("Email", text: $email)
+ Button("Forgot password?") {
+ Task { await createSignIn(email: email) }
+ }
}
}
}
}
- }
-
- extension ForgotPasswordView {
-
- func createSignIn(email: String) async {
- do {
- // Start the sign in reset password process
- try await SignIn.create(strategy: .identifier(email, strategy: "reset_password_email_code"))
- } catch {
- // See https://clerk.com/docs/custom-flows/error-handling
- // for more info on error handling
- dump(error)
+
+ extension ForgotPasswordView {
+
+ func createSignIn(email: String) async {
+ do {
+ // Start the sign in reset password process
+ try await SignIn.create(strategy: .identifier(email, strategy: "reset_password_email_code"))
+ } 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 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)
+ 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](/docs/security/password-protection#reject-compromised-passwords-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
+
+{/* TODO: Add iOS and vanilla JS example. */}
+
+
+
+ ```tsx {{ filename: 'app/forgot-password.tsx', collapsible: true }}
+ 'use client'
+ import React, { useState, useEffect } from 'react'
+ import { 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 { 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 })
+ setError('')
+ } else {
+ console.log(result)
+ }
+ })
+ .catch((err) => {
+ console.error('error', err.errors[0].longMessage)
+ setError(err.errors[0].longMessage)
+ })
+ }
+
+ return (
+
+
Forgot Password?
+
+
+ )
+ }
+
+ export default ForgotPasswordPage
+ ```
+
+
diff --git a/docs/references/javascript/sign-in.mdx b/docs/references/javascript/sign-in.mdx
index a7ef4480e5..4a65ed5206 100644
--- a/docs/references/javascript/sign-in.mdx
+++ b/docs/references/javascript/sign-in.mdx
@@ -680,7 +680,7 @@ For a comprehensive example, see the [custom flow for multi-factor authenticatio
### `resetPassword()`
-Resets a user's password.
+Resets a user's password. It's recommended to use the [custom flow for resetting a user's password](/docs/custom-flows/forgot-password) instead.
```typescript
function resetPassword(params: ResetPasswordParams): Promise
@@ -699,7 +699,7 @@ function resetPassword(params: ResetPasswordParams): Promise
- `signOutOfOtherSessions?`
- `boolean | undefined`
- If `true`, log the user out of all other authenticated sessions.
+ If `true`, signs the user out of all other authenticated sessions.
#### Example