Skip to content

Commit 72ae400

Browse files
Add password reset task documentation and compromised password error handling updates (#2853)
Co-authored-by: Alexis Aguilar <[email protected]>
1 parent d8cd56e commit 72ae400

File tree

7 files changed

+598
-0
lines changed

7 files changed

+598
-0
lines changed

docs/_tooltips/update.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See the [**Updates**](https://dashboard.clerk.com/~/updates) page in the Clerk Dashboard to see the available updates for your instance.

docs/guides/configure/session-tasks.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following table lists the available tasks and their corresponding keys.
1717
| Setting | Key | Description |
1818
| - | - | - |
1919
| [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](!personal-account) instead of an Organization. |
20+
| [Force password reset](/docs/guides/secure/password-protection-and-rules#manually-set-a-password-as-compromised) | `reset-password` | [Enabled by default for instances created after December 8, 2025](!update). When enabled, the user is required to reset their password on their next sign-in if their password is marked as compromised. If your instance is older than December 8, 2025, you will need to update your instance to the **Reset password session task** update. |
2021

2122
## Session states
2223

@@ -37,6 +38,7 @@ The following table lists the available tasks and their corresponding components
3738
| Name | Component |
3839
| - | - |
3940
| [Personal Accounts disabled (default)](/docs/guides/organizations/configure#enable-organizations) | [`<TaskChooseOrganization />`](/docs/reference/components/authentication/task-choose-organization) |
41+
| [Force password reset](/docs/guides/secure/password-protection-and-rules#manually-set-a-password-as-compromised) | [`<TaskResetPassword />`](/docs/reference/components/authentication/task-reset-password) |
4042

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

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ 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 set a password as compromised
54+
55+
Clerk provides a way to manually set a password 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+
To manually set a user's password as compromised:
61+
62+
1. In the Clerk Dashboard, navigate to [**Users**](https://dashboard.clerk.com/~/users) page and select the user you want to mark as compromised. You'll be redirected to the user's settings.
63+
1. In the **Password** section, if a password is set, select the three dots icon and select **Set password compromised**. A modal will appear asking you to confirm the action. Complete the instructions.
64+
65+
> [!IMPORTANT]
66+
> Setting a user's password as compromised will prevent the user from signing in until they reset their password. If you are implementing [custom authentication flows](!custom-flow), 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.

docs/manifest.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,6 +3185,10 @@
31853185
"title": "`<TaskChooseOrganization />`",
31863186
"href": "/docs/reference/components/authentication/task-choose-organization"
31873187
},
3188+
{
3189+
"title": "`<TaskResetPassword />`",
3190+
"href": "/docs/reference/components/authentication/task-reset-password"
3191+
},
31883192
{
31893193
"title": "`<Waitlist />`",
31903194
"href": "/docs/reference/components/authentication/waitlist"

0 commit comments

Comments
 (0)