proposed core.password-reset

Reset a password, the mailbox first, never past another authenticator

A reset asks for an address, or takes the one the sign-in holds. The client must allow the reset flow, on every round, and the request must not ask for a fresh sign-in. The engine mails an 8-digit recovery code through the capped sender, only to an enabled account with a password and a proved address, and asks for it with the same step for every address. A right code proves the mailbox and fixes the account. An account with a passkey or a TOTP proves it next. Then the engine asks for a new password, checks it against the realm's policy and breach list, and in one database transaction replaces the hash, verifies the address, clears the password's disabled mark, revokes every refresh family and advances the session epoch; the user's sessions end, the password's counters clear and the notice is mailed. The reset completes with no amr and no auth_time.

What this view shows

a reset's rounds, from the round that starts it to the password replaced, round by round.

Open world: what is not drawn here is not ruled out. Only the guarantees below are binding.

Left out of this view:

The main story

When: The client may run the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, an enabled account holds the address with a password, its address proved, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the reset started on its own, with no address yet.

  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: the client may reset
  3. Go on from where the transaction is: nothing is named yet: the password_reset step, alone
  4. Ask for the address: the person gives an address
  5. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  6. Read whether the client may run the reset flow: the client may reset
  7. Go on from where the transaction is: the round gives the address: it is checked, normalised and kept, and never changes
  8. Read who holds the address, for the mail: the holder is read
  9. Issue an 8-digit recovery code and ask the capped sender to mail it: no account, no password, a disabled user or an unclaimed account: the code is held, nothing is mailed
  10. Ask for the code: the person types the code from the mail
  11. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  12. Read whether the client may run the reset flow: the client may reset
  13. Go on from where the transaction is: the round answers the code with the right one
  14. Compare the right answer with the held code, in constant time: the code is right
  15. Read the address's holder again, before the right code is taken: the code is taken: consumed, email_code joins the verified steps with no amr, the account is fixed
  16. Read the account again, every authenticator included: every authenticator is proved: the new password is asked for
  17. Ask for a new password: the person types a new password
  18. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  19. Read whether the client may run the reset flow: the client may reset
  20. Go on from where the transaction is: the mailbox is proved: the account is read again
  21. Read the account again, every authenticator included: every authenticator is proved, and the round brings a new password
  22. Check the new password against the realm's password policy: the policy accepts it
  23. Check the new password against the realm's breach list: the new password is not on the list, or the realm checks none
  24. Replace the password in one database transaction: the password is replaced, the families revoked and the epoch advanced, all committed together
  25. End the user's sessions and clear the password's counters, then send the notice: the sessions end, the counters clear, the notice is queued, new_password is audited as passed

Result: Complete the reset.

The flow

a step or a port is missing: the engine answers a redirect, and the transaction ends the request asks for a fresh sign-in: the engine answers a redirect, and the transaction ends every step is served, and no fresh sign-in is asked for the client may not reset: refused, audited as reset_closed the client may reset the mailbox is proved: the account is read again the round answers the code with the right one the round answers the code with another one the round asks for a new code, a minute after the last; the count of wrong answers stays the transaction holds the address but no code yet: the first code the round gives the address: it is checked, normalised and kept, and never changes the round carries no answer to the code: the same step, nothing mailed nothing is named yet: the password_reset step, alone the holder is read a tier that answers 429 is full: nothing is mailed or kept, send_capped is audited an enabled account with a password: the code is queued no account, no password, a disabled user or an unclaimed account: the code is held, nothing is mailed the code is right refused with invalid_credentials, audited as invalid_code no account, no password, a disabled user or an unclaimed account: refused with invalid_credentials, counted, audited the code is taken: consumed, email_code joins the verified steps with no amr, the account is fixed the account is disabled or gone: the transaction ends, audited, nothing written the account holds an authenticator the transport cannot serve: a redirect, the transaction ends the round answers the other authenticator the account holds a passkey or a TOTP: it is asked for next every authenticator is proved, and the round brings a new password every authenticator is proved: the new password is asked for the answer is wrong: refused with invalid_credentials, audited the authenticator is proved: the new password is asked for the policy refuses it: the step again with password_rejected and its reasons the policy accepts it the new password is listed: refused as blocklisted, audited as password_breached the new password is not on the list, or the realm checks none fails: the check fails: counted and recorded, and the new password is accepted the account was disabled meanwhile: nothing is written, the transaction ends the password is replaced, the families revoked and the epoch advanced, all committed together the sessions end, the counters clear, the notice is queued, new_password is audited as passed fails: a store fails after the commit: logged; the epoch refuses the older sessions all the same the next round carries no answer the person gives an address the next round carries no answer: the same step, nothing mailed the person types the code from the mail the person types another code the person asks for a new code: an answer with no otp the next round carries no answer the person types the right code the person types another wrong code the person asks for a new code the next round carries no answer the person proves the authenticator the person answers it wrongly the next round carries no answer the person proves the authenticator the person answers it wrongly again the next round carries no answer: the same step the person types a new password the next round carries no answer the person types another new password start A reset needs the password_reset, email_code and new_password steps, and the engine needs its reset, email-code and password-update ports. A request that asks for a fresh sign-in (max_age, prompt=login) never resets: a reset proves no authenticator, so the person goes on in a browser sign-in instead. Ask whether the reset can be served here at all checkingSteps · server step On every round, before anything is mailed or written (ADR-017 Decision 8): the client is read again, so a client disabled or suspended meanwhile allows nothing. A store error refuses the round. Read whether the client may run the reset flow readingGate · server step The transaction says how far the reset got: a proved mailbox, a held code, an address, or nothing yet. The round's answer goes to the step it answers; an answer to another step is ignored, as every eager answer is. Go on from where the transaction is dispatching · server step Before every code it mints, the first and every resend: whether an enabled account holds the address with a password, and whether that address was proved. It decides only the mail; the step and the answer are the same for every address. Read who holds the address, for the mail readingMailHolder · server step Eight digits from a CSPRNG and a salt of their own; the transaction keeps the salt and the keyed HMAC, never the code, and a new code keeps the count of wrong answers. The send caps count the request for every address before anything is kept. Only an enabled account with a password and a proved address is mailed; every other address gets nothing, its code still held so that the step is the same. Issue an 8-digit recovery code and ask the capped sender to mail it issuingCode · server step After the code's own rules (flows/core/account-verification): the limiters, then the email-code ladder, then the answer's HMAC under the code's salt against the held one, in constant time. It is right; whether it is taken depends on the holder. Compare the right answer with the held code, in constant time comparingCode · server step The answer's HMAC differs from the held one: the transaction counts one wrong answer more, and the ladder one failure more. The 5th of a transaction burns the code (flows/core/account-verification). Compare a wrong answer with the held code, in constant time comparingWrongCode · server step A right code is taken only for an enabled account with a password and a proved address. For any other address it is answered exactly as a wrong one, and counted as one: the person learns nothing, and nothing of a disabled user or an unclaimed account changes (audited as account_disabled or account_unclaimed). Taken, the code fixes the transaction's account. A store error refuses the round; the code stays live. Read the address's holder again, before the right code is taken readingAnswerHolder · server step On every round once the mailbox is proved. A disabled account, or one no longer there, ends the reset with nothing written. An account that holds a passkey or a TOTP, a disabled one included, proves it before any new password: an email code never stands in for an authenticator. One the transport cannot serve sends the person elsewhere. Read the account again, every authenticator included readingAccount · server step The step's own handler, as at the sign-in's rule 8: its limiters and ladder, then its verification, for the account the code fixed. A proof of another account ends the transaction. Verify the account's other authenticator verifyingFactor · server step The one password policy (ADR-017 Decision 8, #634): the length as typed, NFC, the realm's minimum length, the whole-password blocklist with the address and the service and realm names as context words, and the score floor. Check the new password against the realm's password policy checkingNewPassword · server step The same check as every new password's, on the password in NFC, when the realm names a source. A listed one is refused as blocklisted. A check that fails accepts it: counted on the metric and recorded in the audit log. Check the new password against the realm's breach list checkingNewBreachList · server step The user is read again and must be enabled. Then, in one database transaction: the password credential's hash and normalisation marker through the credential repository's password update, which also marks its address verified and clears its disabled mark; the email attribute verified; the update_password required action removed; every refresh family of the user revoked; the user's session epoch advanced; and the reset recorded. A second factor's counter and disabled mark are never touched. A store error writes nothing. Replace the password in one database transaction resetting · server step After the commit: every browser session of the user created before the epoch ends in Postgres and in its cache, the epoch's cache entry is replaced, the password's failure-ladder counter is cleared (never a second factor's), and the "password changed" notice is queued, once. A store error here is logged and changes nothing: the epoch in Postgres already refuses every older session. End the user's sessions and clear the password's counters, then send the notice endingSessions · server step The password_reset step alone; its answer carries the address as login_hint. The request's login_hint fills it in. Ask for the address offeringStart · server step The same step and the same personal data for every address, answered no earlier than the floor: nothing in the answer, its body or its time, says whether an account holds the address. Only the mail differs. Ask for the code offeringCode · server step The same step with invalid_credentials; the code stays, and so does the count of wrong answers. A right code for an address the reset may not change lands here too, alike. Ask for the code again codeRefused · server step The passkey or the TOTP the account holds, as the sign-in's rule 8 offers it; never an email code in its place. Ask for the account's other authenticator offeringFactor · server step The same step with invalid_credentials. Ask for the other authenticator again factorRefused · server step The new_password step with the purpose reset, the realm's minimum length and the longest password accepted; the personal data is the transaction's address. Ask for a new password offeringNewPassword · server step The same step with password_rejected and the reasons: too_short, too_long, blocklisted (a whole dictionary or context word, or a password on the realm's breach list) or too_weak. Ask for a new password again newPasswordRefused · server step The password is replaced, and the transaction ends with it, so nothing completes twice. A reset proves the mailbox, not an authenticator (ADR-017 Decision 10): the transport sets no session, and the code it mints carries no auth_time and no amr, so it never satisfies max_age, prompt=login or a step-up. Complete the reset ends here The transaction stays as the round found it. The Interaction API answers it as an interaction that has expired. Refuse a reset the client may not run ends here The transport cannot serve a step the reset needs, the engine lacks a port, or the account holds an authenticator the transport cannot serve. The transaction ends, and nothing is written. Continue elsewhere ends here A reset under max_age or prompt=login: the person signs in in a browser instead. The transaction ends, and nothing is mailed or written. Continue elsewhere, for a request that asks for a fresh sign-in ends here The same 429 for every address. A code the transaction held stays live. Refuse the round for the send caps ends here The account the code fixed is disabled or no longer there. Nothing is written and nothing is mailed; the FiPA endpoint answers invalid_session, and the Interaction API starts again. End the transaction for an account disabled or gone ends here
a step or a port is missing: the engine answers a redirect, and the transaction ends the request asks for a fresh sign-in: the engine answers a redirect, and the transaction ends every step is served, and no fresh sign-in is asked for the client may not reset: refused, audited as reset_closed the client may reset fails: the client cannot be read the mailbox is proved: the account is read again the round answers the code with the right one the round answers the code with another one the round asks for a new code, a minute after the last; the count of wrong answers stays the transaction holds the address but no code yet: the first code the round gives the address: it is checked, normalised and kept, and never changes the round carries no answer to the code: the same step, nothing mailed nothing is named yet: the password_reset step, alone the holder is read fails: the account directory fails; nothing is mailed a tier that answers 429 is full: nothing is mailed or kept, send_capped is audited an enabled account with a password: the code is queued no account, no password, a disabled user or an unclaimed account: the code is held, nothing is mailed fails: the send-cap store fails: nothing is mailed or kept the code is right refused with invalid_credentials, audited as invalid_code no account, no password, a disabled user or an unclaimed account: refused with invalid_credentials, counted, audited the code is taken: consumed, email_code joins the verified steps with no amr, the account is fixed fails: the account directory fails; the code stays live the account is disabled or gone: the transaction ends, audited, nothing written the account holds an authenticator the transport cannot serve: a redirect, the transaction ends the round answers the other authenticator the account holds a passkey or a TOTP: it is asked for next every authenticator is proved, and the round brings a new password every authenticator is proved: the new password is asked for fails: the account directory fails the answer is wrong: refused with invalid_credentials, audited the authenticator is proved: the new password is asked for fails: a store the step needs fails the policy refuses it: the step again with password_rejected and its reasons the policy accepts it the new password is listed: refused as blocklisted, audited as password_breached the new password is not on the list, or the realm checks none fails: the check fails: counted and recorded, and the new password is accepted the account was disabled meanwhile: nothing is written, the transaction ends the password is replaced, the families revoked and the epoch advanced, all committed together fails: the database fails: nothing is written the sessions end, the counters clear, the notice is queued, new_password is audited as passed fails: a store fails after the commit: logged; the epoch refuses the older sessions all the same the next round carries no answer the person gives an address the next round carries no answer: the same step, nothing mailed the person types the code from the mail the person types another code the person asks for a new code: an answer with no otp the next round carries no answer the person types the right code the person types another wrong code the person asks for a new code the next round carries no answer the person proves the authenticator the person answers it wrongly the next round carries no answer the person proves the authenticator the person answers it wrongly again the next round carries no answer: the same step the person types a new password the next round carries no answer the person types another new password start A reset needs the password_reset, email_code and new_password steps, and the engine needs its reset, email-code and password-update ports. A request that asks for a fresh sign-in (max_age, prompt=login) never resets: a reset proves no authenticator, so the person goes on in a browser sign-in instead. Ask whether the reset can be served here at all checkingSteps · server step On every round, before anything is mailed or written (ADR-017 Decision 8): the client is read again, so a client disabled or suspended meanwhile allows nothing. A store error refuses the round. Read whether the client may run the reset flow readingGate · server step The transaction says how far the reset got: a proved mailbox, a held code, an address, or nothing yet. The round's answer goes to the step it answers; an answer to another step is ignored, as every eager answer is. Go on from where the transaction is dispatching · server step Before every code it mints, the first and every resend: whether an enabled account holds the address with a password, and whether that address was proved. It decides only the mail; the step and the answer are the same for every address. Read who holds the address, for the mail readingMailHolder · server step Eight digits from a CSPRNG and a salt of their own; the transaction keeps the salt and the keyed HMAC, never the code, and a new code keeps the count of wrong answers. The send caps count the request for every address before anything is kept. Only an enabled account with a password and a proved address is mailed; every other address gets nothing, its code still held so that the step is the same. Issue an 8-digit recovery code and ask the capped sender to mail it issuingCode · server step After the code's own rules (flows/core/account-verification): the limiters, then the email-code ladder, then the answer's HMAC under the code's salt against the held one, in constant time. It is right; whether it is taken depends on the holder. Compare the right answer with the held code, in constant time comparingCode · server step The answer's HMAC differs from the held one: the transaction counts one wrong answer more, and the ladder one failure more. The 5th of a transaction burns the code (flows/core/account-verification). Compare a wrong answer with the held code, in constant time comparingWrongCode · server step A right code is taken only for an enabled account with a password and a proved address. For any other address it is answered exactly as a wrong one, and counted as one: the person learns nothing, and nothing of a disabled user or an unclaimed account changes (audited as account_disabled or account_unclaimed). Taken, the code fixes the transaction's account. A store error refuses the round; the code stays live. Read the address's holder again, before the right code is taken readingAnswerHolder · server step On every round once the mailbox is proved. A disabled account, or one no longer there, ends the reset with nothing written. An account that holds a passkey or a TOTP, a disabled one included, proves it before any new password: an email code never stands in for an authenticator. One the transport cannot serve sends the person elsewhere. Read the account again, every authenticator included readingAccount · server step The step's own handler, as at the sign-in's rule 8: its limiters and ladder, then its verification, for the account the code fixed. A proof of another account ends the transaction. Verify the account's other authenticator verifyingFactor · server step The one password policy (ADR-017 Decision 8, #634): the length as typed, NFC, the realm's minimum length, the whole-password blocklist with the address and the service and realm names as context words, and the score floor. Check the new password against the realm's password policy checkingNewPassword · server step The same check as every new password's, on the password in NFC, when the realm names a source. A listed one is refused as blocklisted. A check that fails accepts it: counted on the metric and recorded in the audit log. Check the new password against the realm's breach list checkingNewBreachList · server step The user is read again and must be enabled. Then, in one database transaction: the password credential's hash and normalisation marker through the credential repository's password update, which also marks its address verified and clears its disabled mark; the email attribute verified; the update_password required action removed; every refresh family of the user revoked; the user's session epoch advanced; and the reset recorded. A second factor's counter and disabled mark are never touched. A store error writes nothing. Replace the password in one database transaction resetting · server step After the commit: every browser session of the user created before the epoch ends in Postgres and in its cache, the epoch's cache entry is replaced, the password's failure-ladder counter is cleared (never a second factor's), and the "password changed" notice is queued, once. A store error here is logged and changes nothing: the epoch in Postgres already refuses every older session. End the user's sessions and clear the password's counters, then send the notice endingSessions · server step The password_reset step alone; its answer carries the address as login_hint. The request's login_hint fills it in. Ask for the address offeringStart · server step The same step and the same personal data for every address, answered no earlier than the floor: nothing in the answer, its body or its time, says whether an account holds the address. Only the mail differs. Ask for the code offeringCode · server step The same step with invalid_credentials; the code stays, and so does the count of wrong answers. A right code for an address the reset may not change lands here too, alike. Ask for the code again codeRefused · server step The passkey or the TOTP the account holds, as the sign-in's rule 8 offers it; never an email code in its place. Ask for the account's other authenticator offeringFactor · server step The same step with invalid_credentials. Ask for the other authenticator again factorRefused · server step The new_password step with the purpose reset, the realm's minimum length and the longest password accepted; the personal data is the transaction's address. Ask for a new password offeringNewPassword · server step The same step with password_rejected and the reasons: too_short, too_long, blocklisted (a whole dictionary or context word, or a password on the realm's breach list) or too_weak. Ask for a new password again newPasswordRefused · server step The password is replaced, and the transaction ends with it, so nothing completes twice. A reset proves the mailbox, not an authenticator (ADR-017 Decision 10): the transport sets no session, and the code it mints carries no auth_time and no amr, so it never satisfies max_age, prompt=login or a step-up. Complete the reset ends here The transaction stays as the round found it. The Interaction API answers it as an interaction that has expired. Refuse a reset the client may not run ends here The transport cannot serve a step the reset needs, the engine lacks a port, or the account holds an authenticator the transport cannot serve. The transaction ends, and nothing is written. Continue elsewhere ends here A reset under max_age or prompt=login: the person signs in in a browser instead. The transaction ends, and nothing is mailed or written. Continue elsewhere, for a request that asks for a fresh sign-in ends here The same 429 for every address. A code the transaction held stays live. Refuse the round for the send caps ends here The account the code fixed is disabled or no longer there. Nothing is written and nothing is mailed; the FiPA endpoint answers invalid_session, and the Interaction API starts again. End the transaction for an account disabled or gone ends here The gate, the account directory, the send caps, the other authenticator's store or the database cannot be reached: fail closed. The transaction stays as far as the round took it. Refuse the round while a store is down ends here

blue line: the main storyred dashed line: a failure path (shown with “Show failure paths”)dotted line: decided by a rule, nobody actsdashed grey box: the server decides, nothing is shown

What each step answers

StepThe outside world sees
Ask for the address
offeringStart
kind: step · body: {"current":{"step":"password_reset","params":{}},"alternatives":[],"personal":{}}
Ask for the code
offeringCode
kind: step · body: {"current":{"step":"email_code","params":{"purpose":"recovery","length":8,"expires_in":"the seconds the code has left, at most 600","resend_after":"the seconds until another code may be asked for, at most 60"}},"alternatives":[],"personal":{"identifier":"the transaction's address"}}
Ask for the code again
codeRefused
kind: step · body: {"current":{"step":"email_code","params":{"purpose":"recovery","length":8,"expires_in":"the seconds the code has left, at most 600","resend_after":"the seconds until another code may be asked for, at most 60"},"error":"invalid_credentials"},"alternatives":[],"personal":{"identifier":"the transaction's address"}}
Ask for the account's other authenticator
offeringFactor
kind: step · body: {"current":{"step":"the account's webauthn_get or totp step","params":"the step's own"},"alternatives":"the account's other authenticators the transport serves","personal":"the step's own"}
Ask for the other authenticator again
factorRefused
kind: step · body: {"current":{"step":"the account's webauthn_get or totp step","params":"the step's own","error":"invalid_credentials"},"alternatives":"the account's other authenticators the transport serves","personal":"the step's own"}
Ask for a new password
offeringNewPassword
kind: step · body: {"current":{"step":"new_password","params":{"purpose":"reset","min_length":"the realm's password.minLength","max_length":256}},"alternatives":[],"personal":{"identifier":"the transaction's address"}}
Ask for a new password again
newPasswordRefused
kind: step · body: {"current":{"step":"new_password","params":{"purpose":"reset","min_length":"the realm's password.minLength","max_length":256,"reasons":"why the new password was refused"},"error":"password_rejected"},"alternatives":[],"personal":{"identifier":"the transaction's address"}}
Complete the reset
recovered
kind: complete · body: {"transaction":null,"subject":"the account's user id","identifier":"the transaction's address, normalised","recovered":true}
Refuse a reset the client may not run
resetClosed
kind: refused · body: {"reason":"request_refused","code":"reset_not_allowed"}
Continue elsewhere
redirected
kind: redirect · body: {"reason":"step_not_available","transaction":null}
Continue elsewhere, for a request that asks for a fresh sign-in
freshSignInRequired
kind: redirect · body: {"reason":"request","transaction":null}
Refuse the round for the send caps
sendCapped
kind: refused · body: {"reason":"throttled","retryAfterSeconds":"the send cap's"}
End the transaction for an account disabled or gone
accountEnded
kind: refused · body: {"reason":"transaction_ended","transaction":null}
Refuse the round while a store is down
unavailable
kind: refused · body: {"reason":"unavailable"}

How it decides what happens next

Ask whether the reset can be served here at all

#IfThen
1a step or a port is missing: the engine answers a redirect, and the transaction endsContinue elsewhere
2the request asks for a fresh sign-in: the engine answers a redirect, and the transaction endsContinue elsewhere, for a request that asks for a fresh sign-in
3every step is served, and no fresh sign-in is asked forRead whether the client may run the reset flow

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1notServedredirected768 of 3648
2freshSignInfreshSignInRequired384 of 3648
3otherwisereadingGate2496 of 3648
notServed
not stepsServed

Go on from where the transaction is

#IfThen
1the mailbox is proved: the account is read againRead the account again, every authenticator included
2the round answers the code with the right oneCompare the right answer with the held code, in constant time
3the round answers the code with another oneCompare a wrong answer with the held code, in constant time
4the round asks for a new code, a minute after the last; the count of wrong answers staysRead who holds the address, for the mail
5the round carries no answer to the code: the same step, nothing mailedAsk for the code
6the transaction holds the address but no code yet: the first codeRead who holds the address, for the mail
7the round gives the address: it is checked, normalised and kept, and never changesRead who holds the address, for the mail
8nothing is named yet: the password_reset step, aloneAsk for the address

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1mailboxProvedreadingAccount576 of 1248
2codeHeld and answer == 'rightCode'comparingCode96 of 1248
3codeHeld and answer == 'wrongCode'comparingWrongCode96 of 1248
4codeHeld and answer == 'resend'readingMailHolder96 of 1248
5codeHeldofferingCode96 of 1248
6addressTakenreadingMailHolder96 of 1248
7answer == 'start'readingMailHolder96 of 1248
8otherwiseofferingStart96 of 1248

Compare the right answer with the held code, in constant time

#IfThen
1the code is rightRead the address's holder again, before the right code is taken

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1otherwisereadingAnswerHolder96 of 96

Compare a wrong answer with the held code, in constant time

#IfThen
1refused with invalid_credentials, audited as invalid_codeAsk for the code again

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1otherwisecodeRefused96 of 96

Check the new password against the realm's password policy

#IfThen
1the policy refuses it: the step again with password_rejected and its reasonsAsk for a new password again
2the policy accepts itCheck the new password against the realm's breach list

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1newPasswordRejectednewPasswordRefused8 of 32
2otherwisecheckingNewBreachList24 of 32
newPasswordRejected
newPassword == 'rejected'

Every way it can end

It ends withWhenWhat happens
Complete the reset
ends here
The client may run the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the sign-in's password step started it, with the address it held
  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: the client may reset
  3. Go on from where the transaction is: the transaction holds the address but no code yet: the first code
  4. Read who holds the address, for the mail: the holder is read
  5. Issue an 8-digit recovery code and ask the capped sender to mail it: no account, no password, a disabled user or an unclaimed account: the code is held, nothing is mailed
  6. Ask for the code: the person types the code from the mail
  7. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  8. Read whether the client may run the reset flow: the client may reset
  9. Go on from where the transaction is: the round answers the code with the right one
  10. Compare the right answer with the held code, in constant time: the code is right
  11. Read the address's holder again, before the right code is taken: the code is taken: consumed, email_code joins the verified steps with no amr, the account is fixed
  12. Read the account again, every authenticator included: every authenticator is proved: the new password is asked for
  13. Ask for a new password: the person types a new password
  14. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  15. Read whether the client may run the reset flow: the client may reset
  16. Go on from where the transaction is: the mailbox is proved: the account is read again
  17. Read the account again, every authenticator included: every authenticator is proved, and the round brings a new password
  18. Check the new password against the realm's password policy: the policy accepts it
  19. Check the new password against the realm's breach list: the new password is not on the list, or the realm checks none
  20. Replace the password in one database transaction: the password is replaced, the families revoked and the epoch advanced, all committed together
  21. End the user's sessions and clear the password's counters, then send the notice: the sessions end, the counters clear, the notice is queued, new_password is audited as passed
Refuse a reset the client may not run
ends here
The client is gone, disabled or suspended, or its policy does not list the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the reset started on its own, with no address yet
  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: the client may not reset: refused, audited as reset_closed
Continue elsewhere
ends here
The client is gone, disabled or suspended, or its policy does not list the reset flow, the transport cannot serve password_reset, email_code or new_password, or the engine lacks the ports, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the reset started on its own, with no address yet
  1. Ask whether the reset can be served here at all: a step or a port is missing: the engine answers a redirect, and the transaction ends
Continue elsewhere, for a request that asks for a fresh sign-in
ends here
The client is gone, disabled or suspended, or its policy does not list the reset flow, the transport serves password_reset, email_code and new_password, the request carries max_age or prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the reset started on its own, with no address yet
  1. Ask whether the reset can be served here at all: the request asks for a fresh sign-in: the engine answers a redirect, and the transaction ends
Refuse the round for the send caps
ends here
The client may run the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, a tier that answers 429 is full, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the sign-in's password step started it, with the address it held
  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: the client may reset
  3. Go on from where the transaction is: the transaction holds the address but no code yet: the first code
  4. Read who holds the address, for the mail: the holder is read
  5. Issue an 8-digit recovery code and ask the capped sender to mail it: a tier that answers 429 is full: nothing is mailed or kept, send_capped is audited
End the transaction for an account disabled or gone
ends here
The client may run the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the sign-in's password step started it, with the address it held
  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: the client may reset
  3. Go on from where the transaction is: the transaction holds the address but no code yet: the first code
  4. Read who holds the address, for the mail: the holder is read
  5. Issue an 8-digit recovery code and ask the capped sender to mail it: no account, no password, a disabled user or an unclaimed account: the code is held, nothing is mailed
  6. Ask for the code: the person types the code from the mail
  7. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  8. Read whether the client may run the reset flow: the client may reset
  9. Go on from where the transaction is: the round answers the code with the right one
  10. Compare the right answer with the held code, in constant time: the code is right
  11. Read the address's holder again, before the right code is taken: the code is taken: consumed, email_code joins the verified steps with no amr, the account is fixed
  12. Read the account again, every authenticator included: every authenticator is proved: the new password is asked for
  13. Ask for a new password: the next round carries no answer: the same step
  14. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  15. Read whether the client may run the reset flow: the client may reset
  16. Go on from where the transaction is: the mailbox is proved: the account is read again
  17. Read the account again, every authenticator included: the account is disabled or gone: the transaction ends, audited, nothing written
Refuse the round while a store is down
ends here
The client is gone, disabled or suspended, or its policy does not list the reset flow, the transport serves password_reset, email_code and new_password, the request carries no max_age and no prompt=login, the send caps let the code through, or drop its mail quietly at the address-and-IP tier, no account of the realm holds the address with a password, the account holds no passkey and no TOTP, the policy accepts it, and the realm's breach list does not hold it or the realm checks none, the reset started on its own, with no address yet
  1. Ask whether the reset can be served here at all: every step is served, and no fresh sign-in is asked for
  2. Read whether the client may run the reset flow: it fails, because the client cannot be read

Each row is also a generated test: set the starting situation, send the events, expect the ending.

What is guaranteed

These hold in all 15316 situations the model can reach, from 1536 different starting situations.

Exact rules
IdNever
nothing-before-the-gatestate in ['offeringStart', 'offeringCode', 'codeRefused', 'offeringFactor', 'factorRefused', 'offeringNewPassword', 'newPasswordRefused', 'recovered', 'sendCapped'] and not resetAllows
never-under-a-fresh-sign-instate in ['offeringStart', 'offeringCode', 'codeRefused', 'offeringFactor', 'factorRefused', 'offeringNewPassword', 'newPasswordRefused', 'recovered'] and freshSignIn
no-change-before-the-mailboxpasswordReplaced and not mailboxProved
email-never-stands-inpasswordReplaced and otherFactor != 'none' and not factorProved
no-change-before-the-passwordpasswordReplaced and not passwordAccepted
only-an-enabled-account-is-mailedmailQueued and holder != 'enabled'
a-disabled-user-is-never-changedstate == 'recovered' and holder != 'enabled'
the-reset-ends-what-the-old-password-openedstate == 'recovered' and not (familiesRevoked and epochAdvanced and noticeQueued)
a-second-factor-is-never-touchedsecondFactorTouched
no-factor-at-resetfactorClaimed
compare-only-a-held-codecodeCompared and not addressTaken
every-refusal-is-auditedstate in ['resetClosed', 'sendCapped', 'codeRefused', 'factorRefused', 'newPasswordRefused', 'accountEnded', 'recovered'] and not audited
every-round-waits-the-floorstate in ['offeringStart', 'offeringCode', 'codeRefused', 'offeringFactor', 'factorRefused', 'offeringNewPassword', 'newPasswordRefused', 'recovered', 'resetClosed', 'redirected', 'freshSignInRequired', 'sendCapped', 'accountEnded', 'unavailable'] and not floorWaited

Reference

All transitions (65)
FromEventGuardToEffectsTaken
checkingStepsrule 1notServedredirectedwaitFlooryes
checkingStepsrule 2freshSignInfreshSignInRequiredwaitFlooryes
checkingStepsrule 3readingGateyes
readingGateDONEclosedresetClosedaudit waitFlooryes
readingGateDONEdispatchingyes
readingGateFAIL.storeunavailablewaitFlooryes
dispatchingrule 1mailboxProvedreadingAccountyes
dispatchingrule 2codeHeld and answer == 'rightCode'comparingCodeyes
dispatchingrule 3codeHeld and answer == 'wrongCode'comparingWrongCodeyes
dispatchingrule 4codeHeld and answer == 'resend'readingMailHolderyes
dispatchingrule 5codeHeldofferingCodewaitFlooryes
dispatchingrule 6addressTakenreadingMailHolderyes
dispatchingrule 7answer == 'start'readingMailHoldertakeAddressyes
dispatchingrule 8offeringStartwaitFlooryes
readingMailHolderDONEissuingCodeyes
readingMailHolderFAIL.storeunavailablewaitFlooryes
issuingCodeDONEcappedsendCappedaudit waitFlooryes
issuingCodeDONEmailableofferingCodeholdCode queueCodeMail waitFlooryes
issuingCodeDONEnotMailableofferingCodeholdCode waitFlooryes
issuingCodeFAIL.storeunavailablewaitFlooryes
comparingCoderule 1readingAnswerHoldercompareCodeyes
comparingWrongCoderule 1codeRefusedcompareCode audit waitFlooryes
readingAnswerHolderDONEnotMailablecodeRefusedaudit waitFlooryes
readingAnswerHolderDONEreadingAccountproveMailbox audityes
readingAnswerHolderFAIL.storeunavailablewaitFlooryes
readingAccountDONEnotMailableaccountEndedaudit waitFlooryes
readingAccountDONEfactorPending and otherFactor == 'unusable'redirectedwaitFlooryes
readingAccountDONEfactorPending and answer in ['rightFactor', 'wrongFactor']verifyingFactoryes
readingAccountDONEfactorPendingofferingFactorwaitFlooryes
readingAccountDONEanswer == 'newPassword'checkingNewPasswordyes
readingAccountDONEofferingNewPasswordwaitFlooryes
readingAccountFAIL.storeunavailablewaitFlooryes
verifyingFactorDONEanswer == 'wrongFactor'factorRefusedaudit waitFlooryes
verifyingFactorDONEofferingNewPasswordproveFactor audit waitFlooryes
verifyingFactorFAIL.storeunavailablewaitFlooryes
checkingNewPasswordrule 1newPasswordRejectednewPasswordRefusedaudit waitFlooryes
checkingNewPasswordrule 2checkingNewBreachListyes
checkingNewBreachListDONEnewPasswordListednewPasswordRefusedaudit waitFlooryes
checkingNewBreachListDONEnewPassword == 'accepted'resettingacceptPasswordyes
checkingNewBreachListFAIL.lookupnewPassword == 'checkFailed'resettingacceptPassword recordBreachFailureyes
resettingDONEnotMailableaccountEndedaudit waitFloornever
resettingDONEendingSessionsreplacePasswordyes
resettingFAIL.storeunavailablewaitFlooryes
endingSessionsDONErecoveredendSessions queueNotice audit waitFlooryes
endingSessionsFAIL.storerecoveredqueueNotice audit waitFlooryes
offeringStartROUNDcheckingStepsnewRound moveOnyes
offeringStartSTARTcheckingStepsnewRound moveOn answerStartyes
offeringCodeROUNDcheckingStepsnewRound moveOnyes
offeringCodeRIGHT_CODEcheckingStepsnewRound moveOn answerRightyes
offeringCodeWRONG_CODEcheckingStepsnewRound moveOn answerWrongyes
offeringCodeRESENDcheckingStepsnewRound moveOn answerResendyes
codeRefusedROUNDcheckingStepsnewRound moveOnyes
codeRefusedRIGHT_CODEcheckingStepsnewRound moveOn answerRightyes
codeRefusedWRONG_CODEcheckingStepsnewRound moveOn answerWrongyes
codeRefusedRESENDcheckingStepsnewRound moveOn answerResendyes
offeringFactorROUNDcheckingStepsnewRound moveOnyes
offeringFactorRIGHT_FACTORcheckingStepsnewRound moveOn answerRightFactoryes
offeringFactorWRONG_FACTORcheckingStepsnewRound moveOn answerWrongFactoryes
factorRefusedROUNDcheckingStepsnewRound moveOnyes
factorRefusedRIGHT_FACTORcheckingStepsnewRound moveOn answerRightFactoryes
factorRefusedWRONG_FACTORcheckingStepsnewRound moveOn answerWrongFactoryes
offeringNewPasswordROUNDcheckingStepsnewRound moveOnyes
offeringNewPasswordNEW_PASSWORDcheckingStepsnewRound moveOn answerNewPasswordyes
newPasswordRefusedROUNDcheckingStepsnewRound moveOnyes
newPasswordRefusedNEW_PASSWORDcheckingStepsnewRound moveOn answerNewPasswordyes