proposed core.sign-in

Decide a sign-in, one round at a time

A transport hands the engine a round: the transaction so far, an answer, and the realm, the client and the request the round is for. The engine reads the realm's and the client's MFA policy, runs the hooks of the limiters, the request checks and the bot challenge, takes the identifier from the request's login_hint or from an answer, and asks for the password. Until a factor succeeds, every answer is the same for every account: an unknown identifier, a password-only, passkey-only, disabled or locked account all get the password step again with invalid_credentials, after one Argon2id verification and the 500 ms floor. A correct password fixes the subject; an account whose address is not proved yet then verifies it with an emailed code, whatever REQUIRE_VERIFIED_ACCOUNT says; the sign-in completes when the account's strongest bound factor and the MFA policy are met, and the transport then mints the session or the code. A right password on the realm's breach list, or an update the account owes, first asks for a new password, after a mailed code when the password is the account's only factor. A different identifier in a later round ends the transaction.

What this view shows

one sign-in transaction of the engine, round by round, from the transport's call to the engine's answer, with the identifier and password steps.

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 authorization request carries no login_hint, an enabled, verified account whose only factor is its password, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation.

  1. Wait for the first round: the first round carries no answer
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): no identifier is held and the round brings none; a password answer is ignored, with no verification and no failure counted, and the floor is waited out
  7. Ask for the identifier: the person types the identifier
  8. Read the realm's and the client's MFA policy: the policy is read
  9. Ask the limiters (rule 3): the limiters let the round through
  10. Ask the transport's request checks (rule 4): the request checks let the round through
  11. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  12. Take the identifier (rule 6): the transaction holds the identifier
  13. Pick the first factor (rule 7): the round answered the identifier, or nothing; the floor is waited out
  14. Ask for the password: the person types the password
  15. Read the realm's and the client's MFA policy: the policy is read
  16. Ask the limiters (rule 3): the limiters let the round through
  17. Ask the transport's request checks (rule 4): the request checks let the round through
  18. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  19. Take the identifier (rule 6): the transaction holds the identifier
  20. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  21. Reserve the attempt on the ladder: the attempt is reserved
  22. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  23. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  24. Read the whole account the password proved

Result: Read the whole account the password proved.

The flow

the first round carries no answer the first round answers the identifier step the first round carries a password without a login_hint the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3 the first round carries an answer whose login_hint is not an identifier the policy is read a limiter refuses the round; the floor is waited out the limiters let the round through the request checks refuse the round; the floor is waited out the request checks let the round through a bot challenge is due and no provider can answer it: refused, never waved through a bot challenge is due; any other answer is ignored, and the floor is waited out a bot challenge is due and the transport does not serve the captcha step: the round goes on, under the delay and the 429 of the limiters and the ladder alone, never a redirect no bot challenge is due the answer's login_hint is not an identifier (an email address of at most 254 bytes); nothing is verified or counted the round brings another identifier than the one the transaction holds, or the request's and the answer's differ: the transaction ends, and subject_mismatch is audited under the identifier HMAC no identifier is held and the round brings none; a password answer is ignored, with no verification and no failure counted, and the floor is waited out the transaction holds the identifier the round answers the password step, the one the engine asks for the round answered the identifier, or nothing; the floor is waited out the identifier is inside a wait: the round is refused, neither verified nor counted, and audited under the identifier HMAC the attempt is reserved the account is read, or none holds the identifier no usable password: the dummy hash is verified, the attempt settles as a failure, invalid_credentials is audited under the identifier HMAC, and the floor is waited out the password is wrong; the attempt settles as a failure, audited under the identifier HMAC the password is right and the user is disabled: refused like a wrong password, audited as account_disabled under the identifier HMAC; no subject is fixed and the user is not changed the password is right: the attempt settles as a success, and the realm's breach list is checked the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere the password is listed: the subject is fixed, and the transaction holds that the password must be replaced before the sign-in completes the password is not on the list: the subject is fixed fails: the list is not loaded or configured, or the lookup fails: the failure is counted and audited, and the subject is fixed as for a password not on the list the account is read again, with every factor the account has a passkey bound: the password does not complete the sign-in the policy requires a second factor, and no second-factor step exists yet (#653): the set-up step, which a later issue builds, and never a code the account's address is not proved yet: it is verified inline every factor the sign-in needs is proved the address is verified, and the amr is the password's alone the code that verified the address in this transaction proved the mailbox: the new password is asked for the account has a second factor: it is asked for before the update; until the TOTP step exists (#653), the engine answers a redirect the password is the account's only factor: a code is mailed to its address first, as a reset asks no update is due: every rule is met the code is right: the mailbox is proved, and the new password is asked for the policy refuses it: the step again with password_rejected and its reasons, audited under the subject the policy accepts it the new password is listed: refused as blocklisted, audited under the subject the new password is not on the list, or the realm checks none fails: the check fails: counted and audited, and the new password is accepted the password is replaced: every rule is met the next round carries no answer the person types the identifier the next round carries a password without a login_hint the next round carries a password and its login_hint the next round carries an answer whose login_hint is not an identifier the next round carries no answer the next round answers the identifier again the person types the password the next round carries a password and a login_hint the next round carries an answer whose login_hint is not an identifier the next round carries no answer the next round answers the identifier again the person tries another password the next round carries a password and a login_hint the next round carries an answer whose login_hint is not an identifier 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 The transport starts a transaction: the Interaction API when /oauth/authorize starts an interaction, the FiPA endpoint on its first request. The first round may already carry an answer (an eager answer). Wait for the first round idle · server step On every round, through the policy interface, never from the transaction, so a change reaches a live transaction. A policy that cannot be read refuses the round before anything else, as a store outage does. Read the realm's and the client's MFA policy readingPolicy · server step The per-end-user-IP, per-bucket and per-client limits of PRO-13 (#636): a round from an IP inside its hard block, or above the client's rounds a minute, is refused. A credential round is reserved on them again before the ladder's reservation, so a delay or a hard limit refuses it before it is verified. With no limiter registered, the round goes on. Ask the limiters (rule 3) checkingLimits · server step response_type, PKCE, acr_values, prompt, scope, negotiation and frozen parameters, as each transport checks them. With no check registered, the round goes on. Ask the transport's request checks (rule 4) checkingRequest · server step PRO-19 (#643; ADR-017 Decision 7): a soft threshold of the limiters or of the ladder asks for one, in addition to the delay, and a pass of an earlier challenge in this transaction answers it until a credential round spends it. A due challenge comes before every other step, so an eager password is ignored and gives no password oracle. A transport that does not serve the captcha step meets the delay and the 429 alone. Ask whether a bot challenge is due (rule 5) checkingBotChallenge · server step From the request's login_hint, else from the answer: an identifier answer, or the login_hint of a password answer. It is checked (an email address of at most 254 bytes) and normalised. The first one is fixed for the transaction; a different one later ends it. No account is read here. Take the identifier (rule 6) takingIdentifier · server step The password, required, with a passkey offered beside it where the transport serves webauthn_get (flows/core/passkey; ADR-017 Decision 4: the same password-required shape for every account). An answer is verified only when its step is the one the engine asks for next: here, a password answer. Pick the first factor (rule 7) choosingFirstFactor · server step The ladder hook of PRO-12, keyed on the identifier HMAC whether or not an account holds it: one atomic operation reads the wait, refuses an attempt inside it, and otherwise counts the attempt as a failure until it settles. A store error refuses the round before the password is checked. Reserve the attempt on the ladder reservingAttempt · server step Its user, its enabled flag, whether its primary identity is proven, and its password. Nothing more, so an account that exists costs and risks one read more than none, and nothing about it reaches the answer before a factor succeeds. Read what the password needs of the account the identifier names readingAccount · server step With no usable password (no account, a passkey-only account, a disabled password) the answer is verified against a dummy hash, so every outcome costs one verification. Every refusal is the same: the password step again with invalid_credentials. A disabled user is refused after its password checked out, and nothing about it changes. An unverified account's right password proves it, whatever REQUIRE_VERIFIED_ACCOUNT says: rule 8 verifies its address. A right password is then checked against the realm's breach list, when it names one. Verify the password, at the cost of one Argon2id verification for every account verifyingPassword · server step Only now, once the password proved the account: QAuth keeps only its hash, so the check runs when the person types the password. The SHA-1 of the password against the operator's local list, or its first five hex characters to the operator's k-anonymity range host through the egress client; nothing else of the password leaves the server. A check that fails never blocks the sign-in: it is counted on the metric, recorded in the audit log, and the sign-in goes on as for a password that is not listed. Check the right password against the realm's breach list checkingBreachList · server step Every factor bound to it, disabled ones included, whether it holds an admin role in the realm, and its pending required actions: what the downgrade rule, the MFA policy and the forced update need, read only once a factor proved the account. Read the whole account the password proved readingProvenAccount · server step The downgrade rule: the transaction must prove the account's strongest bound factor (a passkey above a password), and a disabled factor still counts as bound. Then the MFA policy just read: when it requires a second factor, the transaction's amr must carry mfa, and an account with no usable second factor gets the set-up step, never a code. Then an address not proved yet is verified inline, before a second factor the account has is asked for (ADR-017 Decision 4, rule 8). flows/core/passkey draws the passkey the first rule asks for; the second factor the MFA policy asks for is a later issue's (#653), and until it exists the engine answers a redirect. Apply the rules after a first factor (rule 8) afterFirstFactor · server step flows/core/account-verification, round by round: a 6-digit code mailed to the transaction's identifier through the send caps, asked for with the email_code step and the purpose verify_account. A right code marks the address verified and adds no amr value; the sign-in then goes on with the rules after it. Verify the account's address with an emailed code sign-in flow · core.account-verification After every factor the sign-in needs and the address verified, before any session or code: the password the person typed is on the realm's breach list, or the account holds an update_password required action. The person then chooses a new password, and the password alone never sets one (ADR-017 Decision 4 rule 8, decided 2026-10-23): a second factor the account has comes first, and on an account whose only factor is its password a code mailed to its address. A code this transaction already took for that mailbox, to verify its address, counts: no second code is mailed. Ask whether the password must be replaced first (rule 8) checkingPasswordUpdate · server step The code rounds of flows/core/account-verification, with the purpose recovery and 8 digits, as a reset asks for one: the send caps, the email-code ladder, the 10 minutes, the burn at the 5th wrong answer. A right code proves the mailbox and adds no amr value; without it no new password is asked for, and no session or code is issued. Prove the account's mailbox with an emailed code before the update sign-in flow · core.account-verification 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 the presented password's, on the new 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 In one database transaction, for the subject's enabled account only: the password credential's Argon2id hash of the new password in NFC with its normalisation marker, a disabled mark of the failure ladder cleared, and the account's update_password required action removed. The step joins the transaction's verified steps with no amr value, and is audited as passed under the subject. Replace the password and clear the required action writingPassword · server step The same for everyone: no account was read. Try another way offers nothing before a factor succeeds but what this first step offers every account, which in this issue is nothing. Ask for the identifier offeringIdentifier · server step The same for every account the identifier may name, and for none: no account was read. The personal data is the identifier as the person gave it, normalised. Ask for the password offeringPassword · server step The same answer for an unknown identifier, a wrong password, a passkey-only account, a disabled password and a disabled user, in body and in timing. Ask for the password again passwordRefused · server step The new_password step with the purpose update_required, the realm's minimum length and the longest password accepted; the personal data is the transaction's identifier. No session and no code is issued before it is set. 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 transport mints the session (the Interaction API) or the code (the FiPA endpoint) from what the engine returns; the engine itself writes nothing to the account. The transaction ends with it, so nothing completes twice. Complete the sign-in ends here The downgrade rule, drawn by flows/core/passkey: webauthn_get next, a recovery code beside it on the hosted pages, or a redirect where no step of it can be served. The password alone never completes the sign-in. Ask for the account's passkey ends here A rule after the first factor needs a step that does not exist yet or that the transport cannot serve, the forced update's included. The FiPA endpoint answers redirect_to_web; the Interaction API ends the interaction. The transaction ends with it, and nothing is minted. Continue elsewhere ends here One subject per transaction: a different identifier, or a factor of another user once one is proven, ends it. The transport discards it; the FiPA endpoint answers invalid_session. End the transaction ends here Audited as rate_limited, under the identifier HMAC when the transaction holds one and naming nobody otherwise. The transaction stays as far as the round took it. Refuse the round for a limiter ends here The same answer for every identifier inside a wait, held by an account or not. Neither verified nor counted; the transaction stays as far as the round took it. Refuse the round inside the ladder's wait ends here Audited as request_refused, under the identifier HMAC when the transaction holds one and naming nobody otherwise. Refuse the round for the request checks ends here PRO-19 (#643): the captcha step alone, its puzzle kept in the transaction until it is answered. A right answer leaves a pass that the next credential round spends, and the pending step resumes in the same round; a wrong one is refused with captcha_failed and a new puzzle, and counted as a failed round on the limiters, so a stream of wrong answers meets the hard 429. A provider that cannot check an answer refuses the round (unavailable). The answer, captcha_token, is never logged or audited. Offer the bot challenge ends here A fault of the request, audited as invalid_answer (under the identifier HMAC when the transaction holds one); the transaction stays as far as the round took it. Refuse a malformed answer ends here The policy, the ladder or the account directory cannot be read: the round is refused before any credential is checked (fail closed), and the transaction stays as far as the round took it. Refuse the round while a store is down ends here
the first round carries no answer the first round answers the identifier step the first round carries a password without a login_hint the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3 the first round carries an answer whose login_hint is not an identifier the policy is read fails: the policy cannot be read; the floor is waited out a limiter refuses the round; the floor is waited out the limiters let the round through the request checks refuse the round; the floor is waited out the request checks let the round through a bot challenge is due and no provider can answer it: refused, never waved through a bot challenge is due; any other answer is ignored, and the floor is waited out a bot challenge is due and the transport does not serve the captcha step: the round goes on, under the delay and the 429 of the limiters and the ladder alone, never a redirect no bot challenge is due the answer's login_hint is not an identifier (an email address of at most 254 bytes); nothing is verified or counted the round brings another identifier than the one the transaction holds, or the request's and the answer's differ: the transaction ends, and subject_mismatch is audited under the identifier HMAC no identifier is held and the round brings none; a password answer is ignored, with no verification and no failure counted, and the floor is waited out the transaction holds the identifier the round answers the password step, the one the engine asks for the round answered the identifier, or nothing; the floor is waited out the identifier is inside a wait: the round is refused, neither verified nor counted, and audited under the identifier HMAC the attempt is reserved fails: the ladder store fails; the floor is waited out the account is read, or none holds the identifier fails: the database fails; the reservation stays counted no usable password: the dummy hash is verified, the attempt settles as a failure, invalid_credentials is audited under the identifier HMAC, and the floor is waited out the password is wrong; the attempt settles as a failure, audited under the identifier HMAC the password is right and the user is disabled: refused like a wrong password, audited as account_disabled under the identifier HMAC; no subject is fixed and the user is not changed the password is right: the attempt settles as a success, and the realm's breach list is checked the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere the password is listed: the subject is fixed, and the transaction holds that the password must be replaced before the sign-in completes the password is not on the list: the subject is fixed fails: the list is not loaded or configured, or the lookup fails: the failure is counted and audited, and the subject is fixed as for a password not on the list the account is read again, with every factor fails: the database fails; the subject the round fixed stays in the transaction the account has a passkey bound: the password does not complete the sign-in the policy requires a second factor, and no second-factor step exists yet (#653): the set-up step, which a later issue builds, and never a code the account's address is not proved yet: it is verified inline every factor the sign-in needs is proved the address is verified, and the amr is the password's alone fails: the send caps or the email-code ladder's wait refuse the round fails: the fifth wrong code burned the code, or the account changed: the transaction ends fails: the code is not of the offered length fails: the send caps, the ladder or the database fail the code that verified the address in this transaction proved the mailbox: the new password is asked for the account has a second factor: it is asked for before the update; until the TOTP step exists (#653), the engine answers a redirect the password is the account's only factor: a code is mailed to its address first, as a reset asks no update is due: every rule is met the code is right: the mailbox is proved, and the new password is asked for fails: the send caps or the email-code ladder's wait refuse the round fails: the fifth wrong code burned the code, or the account changed: the transaction ends fails: the code is not of the offered length fails: the send caps, the ladder or the database fail the policy refuses it: the step again with password_rejected and its reasons, audited under the subject the policy accepts it the new password is listed: refused as blocklisted, audited under the subject the new password is not on the list, or the realm checks none fails: the check fails: counted and audited, and the new password is accepted the password is replaced: every rule is met fails: the account was disabled or lost its password credential since the read: nothing is written fails: the database fails: nothing is written, and the step is asked for again by the next round the next round carries no answer the person types the identifier the next round carries a password without a login_hint the next round carries a password and its login_hint the next round carries an answer whose login_hint is not an identifier the next round carries no answer the next round answers the identifier again the person types the password the next round carries a password and a login_hint the next round carries an answer whose login_hint is not an identifier the next round carries no answer the next round answers the identifier again the person tries another password the next round carries a password and a login_hint the next round carries an answer whose login_hint is not an identifier 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 The transport starts a transaction: the Interaction API when /oauth/authorize starts an interaction, the FiPA endpoint on its first request. The first round may already carry an answer (an eager answer). Wait for the first round idle · server step On every round, through the policy interface, never from the transaction, so a change reaches a live transaction. A policy that cannot be read refuses the round before anything else, as a store outage does. Read the realm's and the client's MFA policy readingPolicy · server step The per-end-user-IP, per-bucket and per-client limits of PRO-13 (#636): a round from an IP inside its hard block, or above the client's rounds a minute, is refused. A credential round is reserved on them again before the ladder's reservation, so a delay or a hard limit refuses it before it is verified. With no limiter registered, the round goes on. Ask the limiters (rule 3) checkingLimits · server step response_type, PKCE, acr_values, prompt, scope, negotiation and frozen parameters, as each transport checks them. With no check registered, the round goes on. Ask the transport's request checks (rule 4) checkingRequest · server step PRO-19 (#643; ADR-017 Decision 7): a soft threshold of the limiters or of the ladder asks for one, in addition to the delay, and a pass of an earlier challenge in this transaction answers it until a credential round spends it. A due challenge comes before every other step, so an eager password is ignored and gives no password oracle. A transport that does not serve the captcha step meets the delay and the 429 alone. Ask whether a bot challenge is due (rule 5) checkingBotChallenge · server step From the request's login_hint, else from the answer: an identifier answer, or the login_hint of a password answer. It is checked (an email address of at most 254 bytes) and normalised. The first one is fixed for the transaction; a different one later ends it. No account is read here. Take the identifier (rule 6) takingIdentifier · server step The password, required, with a passkey offered beside it where the transport serves webauthn_get (flows/core/passkey; ADR-017 Decision 4: the same password-required shape for every account). An answer is verified only when its step is the one the engine asks for next: here, a password answer. Pick the first factor (rule 7) choosingFirstFactor · server step The ladder hook of PRO-12, keyed on the identifier HMAC whether or not an account holds it: one atomic operation reads the wait, refuses an attempt inside it, and otherwise counts the attempt as a failure until it settles. A store error refuses the round before the password is checked. Reserve the attempt on the ladder reservingAttempt · server step Its user, its enabled flag, whether its primary identity is proven, and its password. Nothing more, so an account that exists costs and risks one read more than none, and nothing about it reaches the answer before a factor succeeds. Read what the password needs of the account the identifier names readingAccount · server step With no usable password (no account, a passkey-only account, a disabled password) the answer is verified against a dummy hash, so every outcome costs one verification. Every refusal is the same: the password step again with invalid_credentials. A disabled user is refused after its password checked out, and nothing about it changes. An unverified account's right password proves it, whatever REQUIRE_VERIFIED_ACCOUNT says: rule 8 verifies its address. A right password is then checked against the realm's breach list, when it names one. Verify the password, at the cost of one Argon2id verification for every account verifyingPassword · server step Only now, once the password proved the account: QAuth keeps only its hash, so the check runs when the person types the password. The SHA-1 of the password against the operator's local list, or its first five hex characters to the operator's k-anonymity range host through the egress client; nothing else of the password leaves the server. A check that fails never blocks the sign-in: it is counted on the metric, recorded in the audit log, and the sign-in goes on as for a password that is not listed. Check the right password against the realm's breach list checkingBreachList · server step Every factor bound to it, disabled ones included, whether it holds an admin role in the realm, and its pending required actions: what the downgrade rule, the MFA policy and the forced update need, read only once a factor proved the account. Read the whole account the password proved readingProvenAccount · server step The downgrade rule: the transaction must prove the account's strongest bound factor (a passkey above a password), and a disabled factor still counts as bound. Then the MFA policy just read: when it requires a second factor, the transaction's amr must carry mfa, and an account with no usable second factor gets the set-up step, never a code. Then an address not proved yet is verified inline, before a second factor the account has is asked for (ADR-017 Decision 4, rule 8). flows/core/passkey draws the passkey the first rule asks for; the second factor the MFA policy asks for is a later issue's (#653), and until it exists the engine answers a redirect. Apply the rules after a first factor (rule 8) afterFirstFactor · server step flows/core/account-verification, round by round: a 6-digit code mailed to the transaction's identifier through the send caps, asked for with the email_code step and the purpose verify_account. A right code marks the address verified and adds no amr value; the sign-in then goes on with the rules after it. Verify the account's address with an emailed code sign-in flow · core.account-verification After every factor the sign-in needs and the address verified, before any session or code: the password the person typed is on the realm's breach list, or the account holds an update_password required action. The person then chooses a new password, and the password alone never sets one (ADR-017 Decision 4 rule 8, decided 2026-10-23): a second factor the account has comes first, and on an account whose only factor is its password a code mailed to its address. A code this transaction already took for that mailbox, to verify its address, counts: no second code is mailed. Ask whether the password must be replaced first (rule 8) checkingPasswordUpdate · server step The code rounds of flows/core/account-verification, with the purpose recovery and 8 digits, as a reset asks for one: the send caps, the email-code ladder, the 10 minutes, the burn at the 5th wrong answer. A right code proves the mailbox and adds no amr value; without it no new password is asked for, and no session or code is issued. Prove the account's mailbox with an emailed code before the update sign-in flow · core.account-verification 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 the presented password's, on the new 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 In one database transaction, for the subject's enabled account only: the password credential's Argon2id hash of the new password in NFC with its normalisation marker, a disabled mark of the failure ladder cleared, and the account's update_password required action removed. The step joins the transaction's verified steps with no amr value, and is audited as passed under the subject. Replace the password and clear the required action writingPassword · server step The same for everyone: no account was read. Try another way offers nothing before a factor succeeds but what this first step offers every account, which in this issue is nothing. Ask for the identifier offeringIdentifier · server step The same for every account the identifier may name, and for none: no account was read. The personal data is the identifier as the person gave it, normalised. Ask for the password offeringPassword · server step The same answer for an unknown identifier, a wrong password, a passkey-only account, a disabled password and a disabled user, in body and in timing. Ask for the password again passwordRefused · server step The new_password step with the purpose update_required, the realm's minimum length and the longest password accepted; the personal data is the transaction's identifier. No session and no code is issued before it is set. 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 transport mints the session (the Interaction API) or the code (the FiPA endpoint) from what the engine returns; the engine itself writes nothing to the account. The transaction ends with it, so nothing completes twice. Complete the sign-in ends here The downgrade rule, drawn by flows/core/passkey: webauthn_get next, a recovery code beside it on the hosted pages, or a redirect where no step of it can be served. The password alone never completes the sign-in. Ask for the account's passkey ends here A rule after the first factor needs a step that does not exist yet or that the transport cannot serve, the forced update's included. The FiPA endpoint answers redirect_to_web; the Interaction API ends the interaction. The transaction ends with it, and nothing is minted. Continue elsewhere ends here One subject per transaction: a different identifier, or a factor of another user once one is proven, ends it. The transport discards it; the FiPA endpoint answers invalid_session. End the transaction ends here The same 429 for every address at the send caps; inside the email-code ladder's wait the answer is neither compared nor counted. The transaction and a code it holds stay. Refuse a round of the inline verification for the send caps or the email-code ladder ends here Audited under the subject, as code_burned or as the account's change. The Interaction API starts the sign-in again; the email-code ladder counts on across transactions. End the transaction for a burned code or an account that changed ends here Audited as rate_limited, under the identifier HMAC when the transaction holds one and naming nobody otherwise. The transaction stays as far as the round took it. Refuse the round for a limiter ends here The same answer for every identifier inside a wait, held by an account or not. Neither verified nor counted; the transaction stays as far as the round took it. Refuse the round inside the ladder's wait ends here Audited as request_refused, under the identifier HMAC when the transaction holds one and naming nobody otherwise. Refuse the round for the request checks ends here PRO-19 (#643): the captcha step alone, its puzzle kept in the transaction until it is answered. A right answer leaves a pass that the next credential round spends, and the pending step resumes in the same round; a wrong one is refused with captcha_failed and a new puzzle, and counted as a failed round on the limiters, so a stream of wrong answers meets the hard 429. A provider that cannot check an answer refuses the round (unavailable). The answer, captcha_token, is never logged or audited. Offer the bot challenge ends here A fault of the request, audited as invalid_answer (under the identifier HMAC when the transaction holds one); the transaction stays as far as the round took it. Refuse a malformed answer ends here The policy, the ladder or the account directory cannot be read: the round is refused before any credential is checked (fail closed), and 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 showndouble border: another flow

What each step answers

StepThe outside world sees
Ask for the identifier
offeringIdentifier
kind: step · body: {"current":{"step":"identifier","params":{}},"alternatives":[],"personal":{}}
Ask for the password
offeringPassword
kind: step · body: {"current":{"step":"password","params":{}},"alternatives":[],"personal":{"identifier":"the transaction's identifier"}}
Ask for the password again
passwordRefused
kind: step · body: {"current":{"step":"password","params":{},"error":"invalid_credentials"},"alternatives":[],"personal":{"identifier":"the transaction's identifier"}}
Ask for a new password
offeringNewPassword
kind: step · body: {"current":{"step":"new_password","params":{"purpose":"update_required","min_length":"the realm's password.minLength","max_length":256}},"alternatives":[],"personal":{"identifier":"the transaction's identifier"}}
Ask for a new password again
newPasswordRefused
kind: step · body: {"current":{"step":"new_password","params":{"purpose":"update_required","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 identifier"}}
Complete the sign-in
completed
kind: complete · body: {"transaction":null,"subject":"the account's user id","identifier":"the transaction's identifier, normalised","amr":"[\"pwd\"], from the step grammar's transactionAmr over the verified steps","authTime":"when the password was verified, in epoch milliseconds"}
Ask for the account's passkey
passkeyOwed
kind: continue · body: {"next":"flows/core/passkey, from afterFirstFactor"}
Continue elsewhere
redirected
kind: redirect · body: {"reason":"step_not_available","transaction":null}
End the transaction
ended
kind: refused · body: {"reason":"transaction_ended","transaction":null}
Refuse a round of the inline verification for the send caps or the email-code ladder
verificationThrottled
kind: refused · body: {"reason":"throttled","retryAfterSeconds":"the send cap's or the email-code ladder's"}
End the transaction for a burned code or an account that changed
verificationEnded
kind: refused · body: {"reason":"transaction_ended","transaction":null}
Refuse the round for a limiter
throttled
kind: refused · body: {"reason":"throttled","retryAfterSeconds":"the limiter's"}
Refuse the round inside the ladder's wait
lockedOut
kind: refused · body: {"reason":"throttled","retryAfterSeconds":"the ladder's"}
Refuse the round for the request checks
requestRefused
kind: refused · body: {"reason":"request_refused","code":"the check's code, such as unmet_authentication_requirements"}
Offer the bot challenge
botChallengeDue
kind: step · body: {"current":{"step":"captcha","params":"the provider's: the built-in proof of work's challenge and difficulty, or a hosted provider's name and site key"},"alternatives":[],"personal":{}}
Refuse a malformed answer
answerRefused
kind: refused · body: {"reason":"invalid_answer"}
Refuse the round while a store is down
unavailable
kind: refused · body: {"reason":"unavailable"}

How it decides what happens next

Ask the limiters (rule 3)

#IfThen
1a limiter refuses the round; the floor is waited outRefuse the round for a limiter
2the limiters let the round throughAsk the transport's request checks (rule 4)

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1limitedthrottled163840 of 1228800
2otherwisecheckingRequest1064960 of 1228800
limited
gate == 'limited'

Ask the transport's request checks (rule 4)

#IfThen
1the request checks refuse the round; the floor is waited outRefuse the round for the request checks
2the request checks let the round throughAsk whether a bot challenge is due (rule 5)

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1requestRefusedrequestRefused163840 of 1064960
2otherwisecheckingBotChallenge901120 of 1064960
requestRefused
gate == 'requestRefused'

Ask whether a bot challenge is due (rule 5)

#IfThen
1a bot challenge is due and no provider can answer it: refused, never waved throughRefuse the round while a store is down
2a bot challenge is due; any other answer is ignored, and the floor is waited outOffer the bot challenge
3a bot challenge is due and the transport does not serve the captcha step: the round goes on, under the delay and the 429 of the limiters and the ladder alone, never a redirectTake the identifier (rule 6)
4no bot challenge is dueTake the identifier (rule 6)

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1challengeUnanswerableunavailable40960 of 901120
2challengeOfferedbotChallengeDue40960 of 901120
3botChallengeDuetakingIdentifier163840 of 901120
4otherwisetakingIdentifier655360 of 901120
botChallengeDue
gate == 'botChallengeDue'
challengeOffered
gate == 'botChallengeDue' and captchaServed and challengeProvider == 'answers'
challengeUnanswerable
gate == 'botChallengeDue' and captchaServed and challengeProvider == 'down'

Take the identifier (rule 6)

#IfThen
1the answer's login_hint is not an identifier (an email address of at most 254 bytes); nothing is verified or countedRefuse a malformed answer
2the round brings another identifier than the one the transaction holds, or the request's and the answer's differ: the transaction ends, and subject_mismatch is audited under the identifier HMACEnd the transaction
3no identifier is held and the round brings none; a password answer is ignored, with no verification and no failure counted, and the floor is waited outAsk for the identifier
4the transaction holds the identifierPick the first factor (rule 7)

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1malformedAnsweranswerRefused163840 of 819200
2identifierMismatchended163840 of 819200
3noIdentifierofferingIdentifier81920 of 819200
4otherwisechoosingFirstFactor409600 of 819200
malformedAnswer
answer == 'malformed'
identifierMismatch
differentIdentifier and ((identifierHeld and roundBringsIdentifier) or (loginHint == 'given' and answerBringsIdentifier))
noIdentifier
not identifierHeld and not roundBringsIdentifier

Pick the first factor (rule 7)

#IfThen
1the round answers the password step, the one the engine asks forReserve the attempt on the ladder
2the round answered the identifier, or nothing; the floor is waited outAsk for the password

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1passwordAnsweredreservingAttempt143360 of 286720
2otherwiseofferingPassword143360 of 286720
passwordAnswered
answer in ['password', 'passwordWithHint']

Verify the password, at the cost of one Argon2id verification for every account

#IfThen
1no usable password: the dummy hash is verified, the attempt settles as a failure, invalid_credentials is audited under the identifier HMAC, and the floor is waited outAsk for the password again
2the password is wrong; the attempt settles as a failure, audited under the identifier HMACAsk for the password again
3the password is right and the user is disabled: refused like a wrong password, audited as account_disabled under the identifier HMAC; no subject is fixed and the user is not changedAsk for the password again
4the password is right: the attempt settles as a success, and the realm's breach list is checkedCheck the right password against the realm's breach list
5the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhereRead the whole account the password proved

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1noUsablePasswordpasswordRefused32256 of 86016
2wrongPasswordpasswordRefused26880 of 86016
3userDisabledpasswordRefused5376 of 86016
4breachListCheckedcheckingBreachList16128 of 86016
5otherwisereadingProvenAccount5376 of 86016
noUsablePassword
account in ['unknown', 'passkeyOnly', 'passwordDisabled']
wrongPassword
password == 'wrong'
userDisabled
account == 'disabled'
breachListChecked
breachList != 'off'

Apply the rules after a first factor (rule 8)

#IfThen
1the account has a passkey bound: the password does not complete the sign-inAsk for the account's passkey
2the policy requires a second factor, and no second-factor step exists yet (#653): the set-up step, which a later issue builds, and never a codeContinue elsewhere
3the account's address is not proved yet: it is verified inlineVerify the account's address with an emailed code
4every factor the sign-in needs is provedAsk whether the password must be replaced first (rule 8)

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1passkeyBoundpasskeyOwed5376 of 21504
2mfaRequiredredirected8064 of 21504
3unverifiedverifyingAccount2688 of 21504
4otherwisecheckingPasswordUpdate5376 of 21504
unverified
account == 'unverified'
passkeyBound
account == 'passwordAndPasskey'
mfaRequired
mfa == 'required'

Ask whether the password must be replaced first (rule 8)

#IfThen
1the code that verified the address in this transaction proved the mailbox: the new password is asked forAsk for a new password
2the account has a second factor: it is asked for before the update; until the TOTP step exists (#653), the engine answers a redirectContinue elsewhere
3the password is the account's only factor: a code is mailed to its address first, as a reset asksProve the account's mailbox with an emailed code before the update
4no update is due: every rule is metComplete the sign-in

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1updateDue and mailboxShownofferingNewPassword1680 of 8064
2updateDue and secondFactorBoundredirected1680 of 8064
3updateDueconfirmingMailbox1680 of 8064
4otherwisecompleted3024 of 8064
updateDue
(breachedHeld or updateRequested) and not passwordUpdated
secondFactorBound
account == 'passwordAndSecondFactor'
mailboxShown
addressVerified or mailboxProved

Check the new password against the realm's password policy

#IfThen
1the policy refuses it: the step again with password_rejected and its reasons, audited under the subjectAsk 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
1newPasswordRejectednewPasswordRefused840 of 3360
2otherwisecheckingNewBreachList2520 of 3360
newPasswordRejected
newPassword == 'rejected'

Every way it can end

It ends withWhenWhat happens
Complete the sign-in
ends here
The authorization request carries no login_hint, an enabled, verified account whose only factor is its password, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the attempt is reserved
  9. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  10. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  11. Read the whole account the password proved: the account is read again, with every factor
  12. Apply the rules after a first factor (rule 8): every factor the sign-in needs is proved
  13. Ask whether the password must be replaced first (rule 8): no update is due: every rule is met
Ask for the account's passkey
ends here
The authorization request carries no login_hint, an enabled, verified account with a password and a passkey; the passkey counts as bound even while it is disabled, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the attempt is reserved
  9. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  10. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  11. Read the whole account the password proved: the account is read again, with every factor
  12. Apply the rules after a first factor (rule 8): the account has a passkey bound: the password does not complete the sign-in
Continue elsewhere
ends here
The authorization request carries no login_hint, an enabled, verified account whose only factor is its password, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, the realm or the client requires a second factor, or requires one for admins and the account is one, the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the attempt is reserved
  9. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  10. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  11. Read the whole account the password proved: the account is read again, with every factor
  12. Apply the rules after a first factor (rule 8): the policy requires a second factor, and no second-factor step exists yet (#653): the set-up step, which a later issue builds, and never a code
End the transaction
ends here
The authorization request carries a login_hint, a valid email address of at most 254 bytes, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, a round brings another identifier than the one the transaction holds (another login_hint, another identifier answer), or the request's login_hint and the answer's differ
  1. Wait for the first round: the first round answers the identifier step
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the round brings another identifier than the one the transaction holds, or the request's and the answer's differ: the transaction ends, and subject_mismatch is audited under the identifier HMAC
Refuse a round of the inline verification for the send caps or the email-code ladder
ends here
The authorization request carries no login_hint, an enabled account with a password whose primary identity, its address, is not proven yet: it is verified inline after its correct password, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the attempt is reserved
  9. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  10. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  11. Read the whole account the password proved: the account is read again, with every factor
  12. Apply the rules after a first factor (rule 8): the account's address is not proved yet: it is verified inline
  13. Verify the account's address with an emailed code: it fails, because the send caps or the email-code ladder's wait refuse the round
End the transaction for a burned code or an account that changed
ends here
The authorization request carries no login_hint, an enabled account with a password whose primary identity, its address, is not proven yet: it is verified inline after its correct password, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the attempt is reserved
  9. Read what the password needs of the account the identifier names: the account is read, or none holds the identifier
  10. Verify the password, at the cost of one Argon2id verification for every account: the password is right and the realm checks no breach list: the attempt settles as a success, the subject is fixed, auth_time is now, and the step is audited with the user as its subject; nothing is checked or sent anywhere
  11. Read the whole account the password proved: the account is read again, with every factor
  12. Apply the rules after a first factor (rule 8): the account's address is not proved yet: it is verified inline
  13. Verify the account's address with an emailed code: it fails, because the fifth wrong code burned the code, or the account changed: the transaction ends
Refuse the round for a limiter
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, a limiter refuses the round (per client, per end-user IP): a 429 with Retry-After, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries no answer
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): a limiter refuses the round; the floor is waited out
Refuse the round inside the ladder's wait
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, the identifier's ladder is inside a wait: the identifier HMAC's counter, whether or not an account holds it, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries a password and its login_hint: the one-call path of ADR-017 Decision 3
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the transaction holds the identifier
  7. Pick the first factor (rule 7): the round answers the password step, the one the engine asks for
  8. Reserve the attempt on the ladder: the identifier is inside a wait: the round is refused, neither verified nor counted, and audited under the identifier HMAC
Refuse the round for the request checks
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, the transport's request checks refuse the round, such as acr_values, prompt=none or a frozen parameter at the FiPA endpoint, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries no answer
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks refuse the round; the floor is waited out
Offer the bot challenge
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, a soft threshold asks for a bot challenge (#643): of the limiters (an end-user IP, its bucket or the client past its free failures) or of the ladder (the round's identifier past the password's or the email code's free failures), while the transaction holds no unspent pass of an earlier challenge, the transport serves the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries no answer
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): a bot challenge is due; any other answer is ignored, and the floor is waited out
Refuse a malformed answer
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries an answer whose login_hint is not an identifier
  2. Read the realm's and the client's MFA policy: the policy is read
  3. Ask the limiters (rule 3): the limiters let the round through
  4. Ask the transport's request checks (rule 4): the request checks let the round through
  5. Ask whether a bot challenge is due (rule 5): no bot challenge is due
  6. Take the identifier (rule 6): the answer's login_hint is not an identifier (an email address of at most 254 bytes); nothing is verified or counted
Refuse the round while a store is down
ends here
The authorization request carries no login_hint, no account in the realm holds the identifier, the answered password is the account's, REQUIRE_VERIFIED_ACCOUNT is on, the default; an unverified account is verified inline, neither the realm nor the client requires a second factor for this account: the default until the policy has its storage (Foundations, the admin console), the realm checks no breach list: password.breachListSource is off, the default, the account holds no update_password required action, the realm's password policy accepts it, and it is not on the realm's breach list or the realm checks none, every hook lets the round through, the transport does not serve the captcha step, the provider can issue a challenge and check its answer, every identifier a round brings is the one the transaction holds, after normalisation
  1. Wait for the first round: the first round carries no answer
  2. Read the realm's and the client's MFA policy: it fails, because the policy cannot be read; the floor is waited out

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

What is guaranteed

These hold in all 8516792 situations the model can reach, from 163840 different starting situations.

Exact rules
IdNever
same-first-response-for-every-accountstate in ['offeringIdentifier', 'offeringPassword'] and accountRead
no-completion-without-a-usable-enabled-accountstate == 'completed' and (noUsablePassword or wrongPassword or userDisabled or (unverified and not addressVerified))
disabled-user-never-gets-a-subjectsubjectFixed and account == 'disabled'
every-refused-password-costs-one-verificationstate == 'passwordRefused' and not (hashVerified or decoyVerified)
unknown-identifier-verifies-the-dummy-hashstate == 'passwordRefused' and noUsablePassword and not decoyVerified
reserve-before-verify(hashVerified or decoyVerified) and not attemptReserved
eager-answer-ignoredstate == 'offeringIdentifier' and (attemptReserved or hashVerified or decoyVerified)
refused-rounds-verify-nothingstate in ['unavailable', 'throttled', 'lockedOut', 'requestRefused', 'botChallengeDue', 'answerRefused'] and (hashVerified or decoyVerified) and not subjectFixed
subject-mismatch-is-auditedstate == 'ended' and not auditedByHmac
no-completion-below-the-strongest-factorstate == 'completed' and (passkeyBound or mfaRequired)
every-round-waits-the-floorstate in ['offeringIdentifier', 'offeringPassword', 'passwordRefused', 'completed', 'redirected', 'ended', 'throttled', 'lockedOut', 'requestRefused', 'botChallengeDue', 'answerRefused', 'unavailable', 'verificationThrottled', 'verificationEnded', 'offeringNewPassword', 'newPasswordRefused'] and not floorWaited
no-raw-identifier-before-a-subjectstate in ['passwordRefused', 'lockedOut', 'ended'] and not subjectFixed and not auditedByHmac
no-completion-while-an-update-is-duestate == 'completed' and (breachedHeld or updateRequested) and not passwordUpdated
the-password-alone-never-sets-a-new-onestate in ['offeringNewPassword', 'newPasswordRefused', 'writingPassword'] and not mailboxShown
the-new-password-passes-the-checkspasswordUpdated and newPassword in ['rejected', 'listed']
a-failed-check-never-blocksbreachCheckFailed and state in ['passwordRefused', 'lockedOut'] and subjectFixed
every-failed-check-is-recordedbreachCheckFailed and subjectFixed and not breachFailureRecorded
nothing-changes-with-the-setting-offbreachList == 'off' and not updateRequested and state in ['checkingBreachList', 'confirmingMailbox', 'offeringNewPassword', 'newPasswordRefused', 'writingPassword']

Reference

All transitions (81)
FromEventGuardToEffectsTaken
idleROUNDreadingPolicynewRound answerNoneyes
idleIDENTIFIERreadingPolicynewRound answerIdentifieryes
idlePASSWORDreadingPolicynewRound answerPasswordyes
idlePASSWORD_WITH_HINTreadingPolicynewRound answerPasswordWithHintyes
idleMALFORMEDreadingPolicynewRound answerMalformedyes
readingPolicyDONEcheckingLimitsyes
readingPolicyFAIL.storeunavailablewaitFlooryes
checkingLimitsrule 1limitedthrottledwaitFlooryes
checkingLimitsrule 2checkingRequestyes
checkingRequestrule 1requestRefusedrequestRefusedwaitFlooryes
checkingRequestrule 2checkingBotChallengeyes
checkingBotChallengerule 1challengeUnanswerableunavailablewaitFlooryes
checkingBotChallengerule 2challengeOfferedbotChallengeDuewaitFlooryes
checkingBotChallengerule 3botChallengeDuetakingIdentifieryes
checkingBotChallengerule 4takingIdentifieryes
takingIdentifierrule 1malformedAnsweranswerRefusedwaitFlooryes
takingIdentifierrule 2identifierMismatchendedauditByHmac waitFlooryes
takingIdentifierrule 3noIdentifierofferingIdentifierwaitFlooryes
takingIdentifierrule 4choosingFirstFactorholdIdentifieryes
choosingFirstFactorrule 1passwordAnsweredreservingAttemptyes
choosingFirstFactorrule 2offeringPasswordwaitFlooryes
reservingAttemptDONElockedOutlockedOutauditByHmac waitFlooryes
reservingAttemptDONEreadingAccountreserveAttemptyes
reservingAttemptFAIL.storeunavailablewaitFlooryes
readingAccountDONEverifyingPasswordreadAccountyes
readingAccountFAIL.storeunavailablewaitFlooryes
verifyingPasswordrule 1noUsablePasswordpasswordRefusedverifyDecoy auditByHmac waitFlooryes
verifyingPasswordrule 2wrongPasswordpasswordRefusedverifyHash auditByHmac waitFlooryes
verifyingPasswordrule 3userDisabledpasswordRefusedverifyHash auditByHmac waitFlooryes
verifyingPasswordrule 4breachListCheckedcheckingBreachListverifyHashyes
verifyingPasswordrule 5readingProvenAccountverifyHash fixSubjectyes
checkingBreachListDONEpasswordListedreadingProvenAccountfixSubject holdBreachedyes
checkingBreachListDONEbreachList == 'notListed'readingProvenAccountfixSubjectyes
checkingBreachListFAIL.lookupbreachCheckFailedreadingProvenAccountfixSubject recordBreachFailureyes
readingProvenAccountDONEafterFirstFactorreadProvenAccountyes
readingProvenAccountFAIL.storeunavailablewaitFlooryes
afterFirstFactorrule 1passkeyBoundpasskeyOwedwaitFlooryes
afterFirstFactorrule 2mfaRequiredredirectedwaitFlooryes
afterFirstFactorrule 3unverifiedverifyingAccountyes
afterFirstFactorrule 4checkingPasswordUpdateyes
verifyingAccountDONEcheckingPasswordUpdateverifyAddressyes
verifyingAccountFAIL.throttledverificationThrottledwaitFlooryes
verifyingAccountFAIL.endedverificationEndedwaitFlooryes
verifyingAccountFAIL.invalidanswerRefusedwaitFlooryes
verifyingAccountFAIL.storeunavailablewaitFlooryes
checkingPasswordUpdaterule 1updateDue and mailboxShownofferingNewPasswordwaitFlooryes
checkingPasswordUpdaterule 2updateDue and secondFactorBoundredirectedwaitFlooryes
checkingPasswordUpdaterule 3updateDueconfirmingMailboxyes
checkingPasswordUpdaterule 4completedwaitFlooryes
confirmingMailboxDONEofferingNewPasswordproveMailbox waitFlooryes
confirmingMailboxFAIL.throttledverificationThrottledwaitFlooryes
confirmingMailboxFAIL.endedverificationEndedwaitFlooryes
confirmingMailboxFAIL.invalidanswerRefusedwaitFlooryes
confirmingMailboxFAIL.storeunavailablewaitFlooryes
checkingNewPasswordrule 1newPasswordRejectednewPasswordRefusedwaitFlooryes
checkingNewPasswordrule 2checkingNewBreachListyes
checkingNewBreachListDONEnewPasswordListednewPasswordRefusedwaitFlooryes
checkingNewBreachListDONEnewPassword == 'accepted'writingPasswordyes
checkingNewBreachListFAIL.lookupnewPassword == 'checkFailed'writingPasswordrecordBreachFailureyes
writingPasswordDONEcompletedupdatePassword waitFlooryes
writingPasswordFAIL.endedverificationEndedwaitFlooryes
writingPasswordFAIL.storeunavailablewaitFlooryes
offeringIdentifierROUNDreadingPolicynewRound answerNoneyes
offeringIdentifierIDENTIFIERreadingPolicynewRound answerIdentifieryes
offeringIdentifierPASSWORDreadingPolicynewRound answerPasswordyes
offeringIdentifierPASSWORD_WITH_HINTreadingPolicynewRound answerPasswordWithHintyes
offeringIdentifierMALFORMEDreadingPolicynewRound answerMalformedyes
offeringPasswordROUNDreadingPolicynewRound answerNoneyes
offeringPasswordIDENTIFIERreadingPolicynewRound answerIdentifieryes
offeringPasswordPASSWORDreadingPolicynewRound answerPasswordyes
offeringPasswordPASSWORD_WITH_HINTreadingPolicynewRound answerPasswordWithHintyes
offeringPasswordMALFORMEDreadingPolicynewRound answerMalformedyes
passwordRefusedROUNDreadingPolicynewRound answerNoneyes
passwordRefusedIDENTIFIERreadingPolicynewRound answerIdentifieryes
passwordRefusedPASSWORDreadingPolicynewRound answerPasswordyes
passwordRefusedPASSWORD_WITH_HINTreadingPolicynewRound answerPasswordWithHintyes
passwordRefusedMALFORMEDreadingPolicynewRound answerMalformedyes
offeringNewPasswordROUNDofferingNewPasswordnewRound waitFlooryes
offeringNewPasswordNEW_PASSWORDcheckingNewPasswordnewRoundyes
newPasswordRefusedROUNDofferingNewPasswordnewRound waitFlooryes
newPasswordRefusedNEW_PASSWORDcheckingNewPasswordnewRoundyes