Skip to content

Commit 87b8e46

Browse files
committed
Add password reset task documentation and error handling updates
- Introduced a new section in the session tasks guide for the password reset task, including its key and description. - Added error handling details for compromised passwords in the custom flows documentation. - Documented the `<TaskResetPassword />` component for rendering the password reset UI. - Updated the password protection guide to include instructions for manually marking passwords as compromised.
1 parent f2d0d29 commit 87b8e46

File tree

5 files changed

+619
-0
lines changed

5 files changed

+619
-0
lines changed

docs/guides/configure/session-tasks.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The following table lists the available tasks and their corresponding keys.
1414
| Setting | Key | Description |
1515
| - | - | - |
1616
| [Allow Personal Accounts](https://dashboard.clerk.com/~/organizations-settings) | `choose-organization` | Disabled by default when enabling Organizations. When disabled, users are required to choose an Organization after authenticating. When enabled, users can choose a Personal Account instead of an Organization. |
17+
| [Password reset](/docs/reference/components/authentication/task-reset-password) | `reset-password` | When the user is required to reset their password on their next sign-in. |
1718

1819
## Session states
1920

@@ -34,6 +35,7 @@ The following table lists the available tasks and their corresponding components
3435
| Name | Component |
3536
| - | - |
3637
| [Personal accounts disabled (default)](/docs/guides/organizations/overview#allow-personal-accounts) | [`<TaskChooseOrganization />`](/docs/reference/components/authentication/task-choose-organization) |
38+
| Password reset | [`<TaskResetPassword />`](/docs/reference/components/authentication/task-reset-password) |
3739

3840
> [!IMPORTANT]
3941
> Personal accounts being disabled by default was released on 08-22-2025. Applications created before this date will not be able to see the **Allow Personal Accounts** setting, because Personal Accounts were enabled by default.

docs/guides/development/custom-flows/error-handling.mdx

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,280 @@ For instance, if you wish to inform a user at which absolute time they will be a
271271
```
272272
</Tab>
273273
</Tabs>
274+
275+
### Password compromised
276+
277+
If you have marked a user's password as compromised and they have another identification method to sign-in, you will receive an HTTP status of `422 (Unprocessable Entity)` and the following error payload:
278+
279+
```json
280+
{
281+
"errors": [
282+
{
283+
"message": "Password compromised",
284+
"long_message": "Your password appears to have been compromised or it's no longer trusted and cannot be used. Please use another method to continue.",
285+
"code": "form_password_compromised",
286+
"meta": {
287+
"name": "param"
288+
}
289+
}
290+
]
291+
}
292+
```
293+
294+
When a user password is marked as compromised, they will not be able to sign in with their compromised password, so you should prompt them to sign-in with another method. If they do not have any other identification methods to sign-in, e.g if they only have username and password, they will be signed in but they will be required to reset their password.
295+
296+
<Tabs items={["Next.js"]}>
297+
<Tab>
298+
This example is written for Next.js App Router but it can be adapted for any React-based framework.
299+
300+
```tsx {{ filename: 'app/sign-in/page.tsx' }}
301+
"use client";
302+
303+
import * as React from "react";
304+
import { useSignIn } from "@clerk/nextjs";
305+
import { useRouter } from "next/navigation";
306+
import {
307+
ClerkAPIError,
308+
EmailCodeFactor,
309+
SignInFirstFactor,
310+
} from "@clerk/types";
311+
import { isClerkAPIResponseError } from "@clerk/nextjs/errors";
312+
313+
const SignInWithEmailCode = () => {
314+
const { isLoaded, signIn, setActive } = useSignIn();
315+
const [errors, setErrors] = React.useState<ClerkAPIError[]>();
316+
const [verifying, setVerifying] = React.useState(false);
317+
const [email, setEmail] = React.useState("");
318+
const [code, setCode] = React.useState("");
319+
const router = useRouter();
320+
321+
async function handleSubmit(e: React.FormEvent) {
322+
e.preventDefault();
323+
324+
if (!isLoaded && !signIn) return null;
325+
326+
try {
327+
// Start the sign-in process using the email code method
328+
const { supportedFirstFactors } = await signIn.create({
329+
identifier: email,
330+
});
331+
332+
// Filter the returned array to find the 'email_code' entry
333+
const isEmailCodeFactor = (
334+
factor: SignInFirstFactor
335+
): factor is EmailCodeFactor => {
336+
return factor.strategy === "email_code";
337+
};
338+
const emailCodeFactor = supportedFirstFactors?.find(isEmailCodeFactor);
339+
340+
if (emailCodeFactor) {
341+
// Grab the emailAddressId
342+
const { emailAddressId } = emailCodeFactor;
343+
344+
// Send the OTP code to the user
345+
await signIn.prepareFirstFactor({
346+
strategy: "email_code",
347+
emailAddressId,
348+
});
349+
350+
// Set verifying to true to display second form
351+
// and capture the OTP code
352+
setVerifying(true);
353+
}
354+
} catch (err) {
355+
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
356+
// for more info on error handling
357+
console.error("Error:", JSON.stringify(err, null, 2));
358+
}
359+
}
360+
361+
async function handleVerification(e: React.FormEvent) {
362+
e.preventDefault();
363+
364+
if (!isLoaded && !signIn) return null;
365+
366+
try {
367+
// Use the code provided by the user and attempt verification
368+
const signInAttempt = await signIn.attemptFirstFactor({
369+
strategy: "email_code",
370+
code,
371+
});
372+
373+
// If verification was completed, set the session to active
374+
// and redirect the user
375+
if (signInAttempt.status === "complete") {
376+
await setActive({
377+
session: signInAttempt.createdSessionId,
378+
navigate: async ({ session }) => {
379+
if (session?.currentTask) {
380+
// Check for tasks and navigate to custom UI to help users resolve them
381+
// See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
382+
console.log(session?.currentTask);
383+
return;
384+
}
385+
386+
router.push("/");
387+
},
388+
});
389+
} else {
390+
// If the status is not complete, check why. User may need to
391+
// complete further steps.
392+
console.error(signInAttempt);
393+
}
394+
} catch (err) {
395+
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
396+
// for more info on error handling
397+
console.error("Error:", JSON.stringify(err, null, 2));
398+
}
399+
}
400+
401+
if (verifying) {
402+
return (
403+
<>
404+
<h1>Verify your email address</h1>
405+
<form onSubmit={handleVerification}>
406+
<label htmlFor="code">Enter your email verification code</label>
407+
<input
408+
value={code}
409+
id="code"
410+
name="code"
411+
onChange={(e) => setCode(e.target.value)}
412+
/>
413+
<button type="submit">Verify</button>
414+
</form>
415+
</>
416+
);
417+
}
418+
419+
return (
420+
<>
421+
<form onSubmit={handleSubmit}>
422+
<label htmlFor="email">Enter email address</label>
423+
<input
424+
value={email}
425+
id="email"
426+
name="email"
427+
type="email"
428+
onChange={(e) => setEmail(e.target.value)}
429+
/>
430+
<button type="submit">Continue</button>
431+
</form>
432+
433+
{errors && (
434+
<ul>
435+
{errors.map((el, index) => (
436+
<li key={index}>{el.longMessage}</li>
437+
))}
438+
</ul>
439+
)}
440+
</>
441+
);
442+
};
443+
444+
export default function SignInForm() {
445+
const { isLoaded, signIn, setActive } = useSignIn();
446+
const [email, setEmail] = React.useState("");
447+
const [password, setPassword] = React.useState("");
448+
const [errors, setErrors] = React.useState<ClerkAPIError[]>();
449+
450+
const router = useRouter();
451+
452+
// Handle the submission of the sign-in form
453+
const handleSignInWithPassword = async (e: React.FormEvent) => {
454+
e.preventDefault();
455+
456+
// Clear any errors that may have occurred during previous form submission
457+
setErrors(undefined);
458+
459+
if (!isLoaded) {
460+
return;
461+
}
462+
463+
// Start the sign-in process using the email and password provided
464+
try {
465+
const signInAttempt = await signIn.create({
466+
identifier: email,
467+
password,
468+
});
469+
470+
// If sign-in process is complete, set the created session as active
471+
// and redirect the user
472+
if (signInAttempt.status === "complete") {
473+
await setActive({
474+
session: signInAttempt.createdSessionId,
475+
navigate: async ({ session }) => {
476+
if (session?.currentTask) {
477+
// Check for tasks and navigate to custom UI to help users resolve them
478+
// See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
479+
console.log(session?.currentTask);
480+
return;
481+
}
482+
483+
router.push("/");
484+
},
485+
});
486+
} else {
487+
// If the status is not complete, check why. User may need to
488+
// complete further steps.
489+
console.error(JSON.stringify(signInAttempt, null, 2));
490+
}
491+
} catch (err) {
492+
if (isClerkAPIResponseError(err)) setErrors(err.errors);
493+
console.error(JSON.stringify(err, null, 2));
494+
}
495+
};
496+
497+
if (errors && errors[0].code === "form_password_compromised") {
498+
return (
499+
<>
500+
<h1>Sign in</h1>
501+
502+
<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>
503+
504+
<SignInWithEmailCode />
505+
</>
506+
);
507+
}
508+
509+
// Display a form to capture the user's email and password
510+
return (
511+
<>
512+
<h1>Sign in</h1>
513+
514+
<form onSubmit={(e) => handleSignInWithPassword(e)}>
515+
<div>
516+
<label htmlFor="email">Enter email address</label>
517+
<input
518+
onChange={(e) => setEmail(e.target.value)}
519+
id="email"
520+
name="email"
521+
type="email"
522+
value={email}
523+
/>
524+
</div>
525+
<div>
526+
<label htmlFor="password">Enter password</label>
527+
<input
528+
onChange={(e) => setPassword(e.target.value)}
529+
id="password"
530+
name="password"
531+
type="password"
532+
value={password}
533+
/>
534+
</div>
535+
<button type="submit">Sign in</button>
536+
</form>
537+
538+
{errors && (
539+
<ul>
540+
{errors.map((el, index) => (
541+
<li key={index}>{el.longMessage}</li>
542+
))}
543+
</ul>
544+
)}
545+
</>
546+
);
547+
}
548+
```
549+
</Tab>
550+
</Tabs>

docs/guides/development/errors/frontend-api.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3316,3 +3316,17 @@ link has expired.
33163316
"code": "waitlist_not_accepting_entries"
33173317
}
33183318
```
3319+
3320+
### <code><wbr />Form<wbr />Pwned<wbr />Password</code>
3321+
3322+
`FormPwnedPassword` signifies an error when the chosen password has been found in the pwned list
3323+
3324+
```json {{ filename: 'Status Code: 422' }}
3325+
{
3326+
"longMessage": "Your password appears to have been compromised or it's no longer trusted and cannot be used. Please use another method to continue.",
3327+
"code": "form_password_compromised",
3328+
"meta": {
3329+
"name": "param"
3330+
}
3331+
}
3332+
```

docs/guides/secure/password-protection-and-rules.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,27 @@ For users that set an average/weak password that complies with your organization
4949

5050
> [!NOTE]
5151
> OWASP recommends providing feedback to users on the strength of their password and offering suggestions for improvement. This can help users create stronger passwords and improve the overall security of the application.
52+
53+
## Manually marking passwords as compromised
54+
55+
Clerk provides a way to manually mark passwords as compromised. This is useful for blocking passwords in the case that:
56+
57+
- The password has recently been added to the compromised password database
58+
- The user was able to set a compromised password because protection was off at the time
59+
60+
> [!NOTE]
61+
> This action will require the user to create a new password on their next sign-in.
62+
> If you are implementing custom authentication flows, you will need to handle the compromised password flow by yourself. See [Error handling](/docs/guides/development/custom-flows/error-handling#password-compromised) for more information.
63+
> If your instance is older than December 8th 2025, you will need to update your instance to the **Reset password session task** update.
64+
> 1. In the Clerk Dashboard, navigate to [**Updates**](https://dashboard.clerk.com/~/updates) page.
65+
> 2. Find the **Reset password session task**, check if the SDK versions mentioned are the ones you are using, if not you will first need to upgrade your SDK's to at least the version's mentioned.
66+
> 3. Once you have upgraded your SDK's, you can update to use the **Reset password session task** by clicking the **Update** button.
67+
68+
To manually mark a user's password as compromised:
69+
70+
1. In the Clerk Dashboard, navigate to [**Users**](https://dashboard.clerk.com/~/users) page and find the user you want to mark as compromised.
71+
1. Click the user's profile and in the password section, if a password is set, you will find the **Mark password as compromised** action, under the three dots menu.
72+
1. Click the **Mark password as compromised** action and a confirmation dialog will appear.
73+
1. You will need to type "Compromised" to confirm the action.
74+
1. Click the **Confirm** button and the user's password will be marked as compromised.
75+
1. Now the user will not be able to sign in with their existing password and will need to create a new password on their next sign-in.

0 commit comments

Comments
 (0)