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?

+
+ {!successfulCreation && ( + <> + + setEmail(e.target.value)} + /> + + + {error &&

{error}

} + + )} + + {successfulCreation && ( + <> + + setPassword(e.target.value)} /> + + + setCode(e.target.value)} /> + + + {error &&

{error}

} + + )} + + {secondFactor &&

2FA is required, but this UI does not handle that

} +
+
+ ) } - return ( -
-

Forgot Password?

-
- {!successfulCreation && ( - <> - - setEmail(e.target.value)} - /> - - - {error &&

{error}

} - - )} - - {successfulCreation && ( - <> - - setPassword(e.target.value)} /> - - - setCode(e.target.value)} /> - - - {error &&

{error}

} - - )} - - {secondFactor &&

2FA is required, but this UI does not handle that

} -
-
- ) - } - - 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?

+
+ {!successfulCreation && ( + <> + + setPhoneNumber(e.target.value)} + /> + + + {error &&

{error}

} + + )} + + {successfulCreation && ( + <> + + setPassword(e.target.value)} /> + + + setCode(e.target.value)} /> + + + {error &&

{error}

} + + )} + + {secondFactor &&

2FA is required, but this UI does not handle that

} +
+
+ ) + } + + 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