proposed core.passkey

Sign in with a passkey, prove it on a passkey-bound account, and bind one after a sign-in

With no login_hint, the first response asks for a discoverable passkey and offers the password and the identifier; with one, it asks for the password and offers the passkey. Either way it is the same for every account, and it reads none. An assertion is reserved on the limiters, its ceremony is taken, and it is verified with user verification; a right one proves its account with ["pop","mfa"]. A password on a passkey-bound account proves nothing that completes the sign-in: the next step is the passkey, a disabled one still counting as bound, and a transport that cannot serve it is sent to the hosted pages. After a sign-in that proved only a password, the hosted pages offer a passkey; a password-only account proves its mailbox with an emailed code before the passkey is bound, the user is told, and the first set of recovery codes is shown.

What this view shows

the webauthn_get step from the first response to a proved passkey, the downgrade rule after a password, and the webauthn_create step from its offer to the passkey bound or declined.

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: LoginHint is absent, servesGet is true, hostedPages is true, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is true, password is right, passkeyBound is true, passkeyUsable is true, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false.

  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: no login_hint: the discoverable passkey, with the password and the identifier offered
  3. Ask for a passkey, with the password and the identifier offered: the person taps a passkey
  4. Reserve the assertion on the limiters: the round is reserved, and counted as a failure until it settles
  5. Take the ceremony and verify the assertion: the assertion is right, with user verification
  6. Read the account the passkey names: a disabled account, before any factor: refused like any other assertion, as invalid_credentials

Result: Ask for the passkey again.

The flow

the transport does not serve webauthn_get: the preferred passkey gives way to the password the transport serves webauthn_get a login_hint: the password, with the passkey offered no login_hint: the discoverable passkey, with the password and the identifier offered a delay or a hard limit refuses the round: nothing is verified the round is reserved, and counted as a failure until it settles no UV flag in the signed authenticator data: refused as invalid_credentials refused as invalid_credentials, the same for every reason another account's passkey: the transaction ends, audited as subject_mismatch the assertion is right, with user verification a disabled account, before any factor: refused like any other assertion, as invalid_credentials the account was disabled after its password: the transaction ends the passkey is proved: webauthn_get joins the verified steps, the subject is fixed, auth_time moves, and the step is audited as passed a wrong password, or none to verify: the password step again, the same for every account the password is right: the subject is fixed and pwd joins the amr the account is read with every factor a passkey-bound account proved its password only: the next step is webauthn_get the transport does not serve webauthn_get, or every passkey of the account is disabled: no step the transport serves can complete the sign-in here a password-only sign-in on the hosted pages: a passkey is offered the strongest bound factor is proved, and no passkey is offered the ceremony is held the ceremony is held refused as invalid_credentials, with a new ceremony the new passkey is verified, and the transaction holds it until it is bound the mailbox is not proved in this transaction: a code is mailed the last factor is more than 5 minutes old: nothing is bound, and the sign-in completes; the account page binds one after a fresh sign-in the binding may go ahead the code is right: the mailbox is proved the passkey is bound, and the user is told the next round carries no answer: the same offer the person taps a passkey the person answers the password or the identifier instead the answer is not a JSON assertion of at most 8192 characters: refused as invalid_answer the next round carries no answer: the same offer the person taps a passkey the person types the password the answer does not fit its step the next round carries no answer: the same offer the person taps the passkey the answer is not a JSON assertion the next round carries no answer: the same offer the answer carries no response: nothing is bound, and the offer is not made again in this transaction the person creates a passkey the answer is not a JSON attestation of at most 16384 characters start Passkey-first: the first response is the same for every account, and no account is read for it. With no login_hint it asks for a discoverable passkey, preferred rather than required, and offers the password and the identifier; with one, it asks for the password and offers the passkey. A transport that does not serve webauthn_get gets the password-and-identifier shape of flows/core/sign-in, never a redirect. Choose the first response (rules 6 and 7) firstRound · server step The transaction's held ceremony is kept while more than a minute of it is left, with the same params. Otherwise a new key from a CSPRNG names a new ceremony: a 32-byte challenge, the realm's RP ID, user verification required, no allowCredentials, kept 5 minutes in the challenge store. The params name no account. Start, or keep, the transaction's assertion ceremony startingCeremony · server step The limiters' credential round (#636) for the client, the end-user IP and the session, before anything of the answer is verified. No per-account ladder: the assertion names no account until it is verified. Reserve the assertion on the limiters reservingAssertion · server step The held ceremony's state is taken from the challenge store first, so a challenge is answered once, right or wrong. The credential is looked up by its id among the realm's passkeys, and a disabled one is refused. The signature, the challenge, the origin, the RP ID hash, user presence, user verification and the backup flags are verified; a sign counter that went back is logged. The step checks user verification again from the verified result. A right assertion updates the passkey's counter and last use. Take the ceremony and verify the assertion verifyingAssertion · server step The account the verified passkey belongs to, with every factor it holds, a disabled one included. Read the account the passkey names readingAccount · server step Drawn by flows/core/sign-in: the ladder's reservation, one Argon2id verification, the breach list. A wrong one gets the same answer for every account, a passkey-bound one included. Verify the password verifyingPassword · server step The account, with every factor it holds, a disabled passkey included. Read the account the password proved readingProvenAccount · server step A sign-in completes only after the account's strongest bound factor: a passkey above a password, a disabled one still counting. An emailed code never stands in for it; on the hosted pages a recovery code may (flows/core/recovery-code). After the rest of rule 8, a sign-in that proved a password on an account with no passkey is offered one, on the hosted pages, while the password is at most 5 minutes old. Apply the downgrade rule, then offer a passkey (rule 8) afterFirstFactor · server step The same ceremony as the first response's: the one held is kept while more than a minute of it is left. Its params name no account, and its answer must be the subject's passkey. Start, or keep, the ceremony for the passkey the account must prove startingDowngrade · server step Creation options for the realm's relying party: a 32-byte challenge, a new random user handle, the account's identifier as its name, a discoverable credential with user verification required, Ed25519, ES256 or RS256, attestation "none", and the account's passkeys as excludeCredentials. Kept 5 minutes in the challenge store. Start, or keep, the transaction's registration ceremony startingRegistration · server step The held ceremony's state is taken first. The attestation object and client data are verified against the challenge, the origins and the RP ID, with user presence and user verification, a discoverable credential and an allowed algorithm. The passkey is not bound yet. Take the ceremony and verify the new passkey verifyingRegistration · server step Binding the first passkey of a password-only account needs a code mailed to its address in the same transaction, unless the transaction proved that mailbox already, and a factor proved at most 5 minutes ago (ADR-017 Decision 8). Ask what the binding still needs checkingBinding · server step The code rounds of flows/core/account-verification, with the purpose recovery and 8 digits: 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. Prove the account's mailbox with an emailed code sign-in flow · core.account-verification One insert under the realm's RP ID, which a passkey of another RP ID never passes, named by its credential id. Then the binding notice of NIST SP 800-63B-4 §4.1.2.1 is mailed, and webauthn_create is audited as passed; the transaction records the binding, not a verified step, so it adds no amr value. The security log's own event of the binding comes with the account pages (#686). Bind the passkey to the account storingPasskey · server step The uniform first response with no login_hint. webauthn_get is preferred, not required, and its params are request options with no allowCredentials, so the browser offers every passkey of the realm's RP ID. Ask for a passkey, with the password and the identifier offered offeringPasskeyFirst · server step The uniform first response with a login_hint: the password step for every account, with or without a passkey, and webauthn_get beside it with the same params as with no hint. Ask for the password, with a passkey offered offeringPasswordFirst · server step After a password on a passkey-bound account. webauthn_get is required and never gives way; on the hosted pages a recovery code is offered beside it when the account holds an unused one (flows/core/recovery-code). Ask a passkey-bound account for its passkey offeringPasskey · server step webauthn_create with optional true: the person may create a passkey or go on without one. Its personal data is the new user handle, the account's identifier and the passkeys to exclude. Offer a passkey offeringEnrolment · server step Rule 9, after whatever rule 8 still asks for. The amr comes from the verified steps: a passkey alone gives ["pop","mfa"], a password then a passkey ["pwd","pop","mfa"]; webauthn_create adds nothing. hwk and swk are never claimed. Complete the sign-in ends here The passkey is the account's first factor beyond the password: before the sign-in completes, flows/core/recovery-code issues a set and shows it once, unless the account holds one the person confirmed. Show the first set of recovery codes ends here On the hosted pages, flows/core/recovery-code offers a recovery code in place of a disabled passkey; with none, or on a transport that cannot serve webauthn_get, the engine answers a redirect, which the FiPA endpoint answers with redirect_to_web and the hosted pages, where no step is left to ask for, with the end of the interaction. The password never completes the sign-in. Go on without a passkey step ends here The transport does not serve webauthn_get: the first response of flows/core/sign-in, password required and identifier offered. Ask for the password and the identifier ends here The person answered a step offered beside the passkey, drawn by flows/core/sign-in. Go on with the password or the identifier ends here Drawn by flows/core/sign-in; the same answer for every account, with webauthn_get offered again. Ask for the password again ends here The same offer with a new ceremony and webauthn_get current with invalid_credentials; nothing says why. Before a factor it is the same for every account. Ask for the passkey again ends here The same offer with a new ceremony, webauthn_create current with invalid_credentials. Offer a passkey again ends here Nothing is verified; the transaction stays, and so does the offer. Refuse the round for a limit ends here One subject per transaction: another account's passkey, or an account disabled or removed after its first factor, ends it. End the transaction ends here A fault of the request, audited as invalid_answer; the transaction and its ceremony stay. Refuse a malformed answer ends here
the transport does not serve webauthn_get: the preferred passkey gives way to the password the transport serves webauthn_get a login_hint: the password, with the passkey offered no login_hint: the discoverable passkey, with the password and the identifier offered fails: the challenge store fails; nothing is offered a delay or a hard limit refuses the round: nothing is verified the round is reserved, and counted as a failure until it settles fails: the limiters fail closed no UV flag in the signed authenticator data: refused as invalid_credentials refused as invalid_credentials, the same for every reason another account's passkey: the transaction ends, audited as subject_mismatch the assertion is right, with user verification fails: the challenge store or the database fails; the ceremony is gone and nothing is proved a disabled account, before any factor: refused like any other assertion, as invalid_credentials the account was disabled after its password: the transaction ends the passkey is proved: webauthn_get joins the verified steps, the subject is fixed, auth_time moves, and the step is audited as passed fails: the database fails; nothing is proved a wrong password, or none to verify: the password step again, the same for every account the password is right: the subject is fixed and pwd joins the amr fails: the verifier or the ladder fails the account is read with every factor fails: the database fails a passkey-bound account proved its password only: the next step is webauthn_get the transport does not serve webauthn_get, or every passkey of the account is disabled: no step the transport serves can complete the sign-in here a password-only sign-in on the hosted pages: a passkey is offered the strongest bound factor is proved, and no passkey is offered the ceremony is held fails: the challenge store fails the ceremony is held fails: the registration cannot start (the challenge store fails): the offer is optional, so it is reported and not made again in this transaction, and the sign-in completes refused as invalid_credentials, with a new ceremony the new passkey is verified, and the transaction holds it until it is bound fails: the challenge store fails the mailbox is not proved in this transaction: a code is mailed the last factor is more than 5 minutes old: nothing is bound, and the sign-in completes; the account page binds one after a fresh sign-in the binding may go ahead the code is right: the mailbox is proved 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 passkey is bound, and the user is told fails: the realm holds that credential id already: refused as invalid_credentials fails: the account was disabled or removed since: the transaction ends fails: the database fails; nothing is bound the next round carries no answer: the same offer the person taps a passkey the person answers the password or the identifier instead the answer is not a JSON assertion of at most 8192 characters: refused as invalid_answer the next round carries no answer: the same offer the person taps a passkey the person types the password the answer does not fit its step the next round carries no answer: the same offer the person taps the passkey the answer is not a JSON assertion the next round carries no answer: the same offer the answer carries no response: nothing is bound, and the offer is not made again in this transaction the person creates a passkey the answer is not a JSON attestation of at most 16384 characters start Passkey-first: the first response is the same for every account, and no account is read for it. With no login_hint it asks for a discoverable passkey, preferred rather than required, and offers the password and the identifier; with one, it asks for the password and offers the passkey. A transport that does not serve webauthn_get gets the password-and-identifier shape of flows/core/sign-in, never a redirect. Choose the first response (rules 6 and 7) firstRound · server step The transaction's held ceremony is kept while more than a minute of it is left, with the same params. Otherwise a new key from a CSPRNG names a new ceremony: a 32-byte challenge, the realm's RP ID, user verification required, no allowCredentials, kept 5 minutes in the challenge store. The params name no account. Start, or keep, the transaction's assertion ceremony startingCeremony · server step The limiters' credential round (#636) for the client, the end-user IP and the session, before anything of the answer is verified. No per-account ladder: the assertion names no account until it is verified. Reserve the assertion on the limiters reservingAssertion · server step The held ceremony's state is taken from the challenge store first, so a challenge is answered once, right or wrong. The credential is looked up by its id among the realm's passkeys, and a disabled one is refused. The signature, the challenge, the origin, the RP ID hash, user presence, user verification and the backup flags are verified; a sign counter that went back is logged. The step checks user verification again from the verified result. A right assertion updates the passkey's counter and last use. Take the ceremony and verify the assertion verifyingAssertion · server step The account the verified passkey belongs to, with every factor it holds, a disabled one included. Read the account the passkey names readingAccount · server step Drawn by flows/core/sign-in: the ladder's reservation, one Argon2id verification, the breach list. A wrong one gets the same answer for every account, a passkey-bound one included. Verify the password verifyingPassword · server step The account, with every factor it holds, a disabled passkey included. Read the account the password proved readingProvenAccount · server step A sign-in completes only after the account's strongest bound factor: a passkey above a password, a disabled one still counting. An emailed code never stands in for it; on the hosted pages a recovery code may (flows/core/recovery-code). After the rest of rule 8, a sign-in that proved a password on an account with no passkey is offered one, on the hosted pages, while the password is at most 5 minutes old. Apply the downgrade rule, then offer a passkey (rule 8) afterFirstFactor · server step The same ceremony as the first response's: the one held is kept while more than a minute of it is left. Its params name no account, and its answer must be the subject's passkey. Start, or keep, the ceremony for the passkey the account must prove startingDowngrade · server step Creation options for the realm's relying party: a 32-byte challenge, a new random user handle, the account's identifier as its name, a discoverable credential with user verification required, Ed25519, ES256 or RS256, attestation "none", and the account's passkeys as excludeCredentials. Kept 5 minutes in the challenge store. Start, or keep, the transaction's registration ceremony startingRegistration · server step The held ceremony's state is taken first. The attestation object and client data are verified against the challenge, the origins and the RP ID, with user presence and user verification, a discoverable credential and an allowed algorithm. The passkey is not bound yet. Take the ceremony and verify the new passkey verifyingRegistration · server step Binding the first passkey of a password-only account needs a code mailed to its address in the same transaction, unless the transaction proved that mailbox already, and a factor proved at most 5 minutes ago (ADR-017 Decision 8). Ask what the binding still needs checkingBinding · server step The code rounds of flows/core/account-verification, with the purpose recovery and 8 digits: 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. Prove the account's mailbox with an emailed code sign-in flow · core.account-verification One insert under the realm's RP ID, which a passkey of another RP ID never passes, named by its credential id. Then the binding notice of NIST SP 800-63B-4 §4.1.2.1 is mailed, and webauthn_create is audited as passed; the transaction records the binding, not a verified step, so it adds no amr value. The security log's own event of the binding comes with the account pages (#686). Bind the passkey to the account storingPasskey · server step The uniform first response with no login_hint. webauthn_get is preferred, not required, and its params are request options with no allowCredentials, so the browser offers every passkey of the realm's RP ID. Ask for a passkey, with the password and the identifier offered offeringPasskeyFirst · server step The uniform first response with a login_hint: the password step for every account, with or without a passkey, and webauthn_get beside it with the same params as with no hint. Ask for the password, with a passkey offered offeringPasswordFirst · server step After a password on a passkey-bound account. webauthn_get is required and never gives way; on the hosted pages a recovery code is offered beside it when the account holds an unused one (flows/core/recovery-code). Ask a passkey-bound account for its passkey offeringPasskey · server step webauthn_create with optional true: the person may create a passkey or go on without one. Its personal data is the new user handle, the account's identifier and the passkeys to exclude. Offer a passkey offeringEnrolment · server step Rule 9, after whatever rule 8 still asks for. The amr comes from the verified steps: a passkey alone gives ["pop","mfa"], a password then a passkey ["pwd","pop","mfa"]; webauthn_create adds nothing. hwk and swk are never claimed. Complete the sign-in ends here The passkey is the account's first factor beyond the password: before the sign-in completes, flows/core/recovery-code issues a set and shows it once, unless the account holds one the person confirmed. Show the first set of recovery codes ends here On the hosted pages, flows/core/recovery-code offers a recovery code in place of a disabled passkey; with none, or on a transport that cannot serve webauthn_get, the engine answers a redirect, which the FiPA endpoint answers with redirect_to_web and the hosted pages, where no step is left to ask for, with the end of the interaction. The password never completes the sign-in. Go on without a passkey step ends here The transport does not serve webauthn_get: the first response of flows/core/sign-in, password required and identifier offered. Ask for the password and the identifier ends here The person answered a step offered beside the passkey, drawn by flows/core/sign-in. Go on with the password or the identifier ends here Drawn by flows/core/sign-in; the same answer for every account, with webauthn_get offered again. Ask for the password again ends here The same offer with a new ceremony and webauthn_get current with invalid_credentials; nothing says why. Before a factor it is the same for every account. Ask for the passkey again ends here The same offer with a new ceremony, webauthn_create current with invalid_credentials. Offer a passkey again ends here Nothing is verified; the transaction stays, and so does the offer. Refuse the round for a limit ends here One subject per transaction: another account's passkey, or an account disabled or removed after its first factor, ends it. End the transaction ends here A fault of the request, audited as invalid_answer; the transaction and its ceremony stay. Refuse a malformed answer ends here 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 showndouble border: another flow

What each step answers

StepThe outside world sees
Ask for a passkey, with the password and the identifier offered
offeringPasskeyFirst
kind: step · body: {"current":{"step":"webauthn_get","required":false,"params":{"challenge":"the ceremony's","rpId":"the realm's","timeout":300000,"userVerification":"required"}},"alternatives":["password","identifier"],"personal":{}}
Ask for the password, with a passkey offered
offeringPasswordFirst
kind: step · body: {"current":{"step":"password","params":{}},"alternatives":["webauthn_get, with the ceremony's params"],"personal":{"identifier":"the transaction's identifier"}}
Ask a passkey-bound account for its passkey
offeringPasskey
kind: step · body: {"current":{"step":"webauthn_get","params":"the ceremony's, as in the first response"},"alternatives":"recovery_code, on the hosted pages","personal":{"identifier":"the transaction's identifier"}}
Offer a passkey
offeringEnrolment
kind: step · body: {"current":{"step":"webauthn_create","params":{"optional":true,"options":"the ceremony's creation options"}},"alternatives":[],"personal":{"user":"{ id, name, displayName }","excludeCredentials":"the account's passkeys"}}
Complete the sign-in
completed
kind: complete · body: {"amr":"[\"pop\",\"mfa\"], [\"pwd\",\"pop\",\"mfa\"] or [\"pwd\"]","authTime":"the last factor's","transaction":null}
Show the first set of recovery codes
owingRecoveryCodes
kind: continue · body: {"next":"flows/core/recovery-code, from checkingSet"}
Go on without a passkey step
passkeyUnavailable
kind: continue · body: {"next":"flows/core/recovery-code, from readingCodes; else a redirect with step_not_available"}
Ask for the password and the identifier
passwordOnlyOffer
kind: step · body: {"current":"password","alternatives":["identifier"]}
Go on with the password or the identifier
otherStepAnswered
kind: continue · body: {"next":"flows/core/sign-in, at the answered step"}
Ask for the password again
passwordRefused
kind: step · body: {"current":{"step":"password","error":"invalid_credentials"},"alternatives":["webauthn_get"]}
Ask for the passkey again
assertionRefused
kind: step · body: {"current":{"step":"webauthn_get","error":"invalid_credentials"}}
Offer a passkey again
registrationRefused
kind: step · body: {"current":{"step":"webauthn_create","error":"invalid_credentials"}}
Refuse the round for a limit
throttled
kind: refused · body: {"reason":"throttled","retryAfterSeconds":"the limit's"}
End the transaction
ended
kind: refused · body: {"reason":"transaction_ended","transaction":null}
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

Choose the first response (rules 6 and 7)

#IfThen
1the transport does not serve webauthn_get: the preferred passkey gives way to the passwordAsk for the password and the identifier
2the transport serves webauthn_getStart, or keep, the transaction's assertion ceremony

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1notServedpasswordOnlyOffer9216 of 18432
2otherwisestartingCeremony9216 of 18432
notServed
not servesGet

Apply the downgrade rule, then offer a passkey (rule 8)

#IfThen
1a passkey-bound account proved its password only: the next step is webauthn_getStart, or keep, the ceremony for the passkey the account must prove
2the transport does not serve webauthn_get, or every passkey of the account is disabled: no step the transport serves can complete the sign-in hereGo on without a passkey step
3a password-only sign-in on the hosted pages: a passkey is offeredStart, or keep, the transaction's registration ceremony
4the strongest bound factor is proved, and no passkey is offeredComplete the sign-in

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1passkeyOwed and passkeyAskablestartingDowngrade576 of 2928
2passkeyOwedpasskeyUnavailable576 of 2928
3enrolmentOfferedstartingRegistration288 of 2928
4otherwisecompleted1488 of 2928
passkeyOwed
passkeyBound and not passkeyProved
passkeyAskable
servesGet and passkeyUsable
enrolmentOffered
hostedPages and passwordProved and not passkeyProved and not passkeyBound and not enrolmentDeclined and authAge == 'fresh'

Ask what the binding still needs

#IfThen
1the mailbox is not proved in this transaction: a code is mailedProve the account's mailbox with an emailed code
2the last factor is more than 5 minutes old: nothing is bound, and the sign-in completes; the account page binds one after a fresh sign-inComplete the sign-in
3the binding may go aheadBind the passkey to the account

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1mailboxOwedprovingMailbox192 of 384
2stalecompleted96 of 384
3otherwisestoringPasskey96 of 384
mailboxOwed
not mailboxProved
stale
authAge == 'stale'

Every way it can end

It ends withWhenWhat happens
Complete the sign-in
ends here
LoginHint is given, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person types the password
  4. Verify the password: the password is right: the subject is fixed and pwd joins the amr
  5. Read the account the password proved: the account is read with every factor
  6. Apply the downgrade rule, then offer a passkey (rule 8): the strongest bound factor is proved, and no passkey is offered
Show the first set of recovery codes
ends here
LoginHint is given, servesGet is true, hostedPages is true, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is true
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person types the password
  4. Verify the password: the password is right: the subject is fixed and pwd joins the amr
  5. Read the account the password proved: the account is read with every factor
  6. Apply the downgrade rule, then offer a passkey (rule 8): a password-only sign-in on the hosted pages: a passkey is offered
  7. Start, or keep, the transaction's registration ceremony: the ceremony is held
  8. Offer a passkey: the person creates a passkey
  9. Take the ceremony and verify the new passkey: the new passkey is verified, and the transaction holds it until it is bound
  10. Ask what the binding still needs: the binding may go ahead
  11. Bind the passkey to the account: the passkey is bound, and the user is told
Go on without a passkey step
ends here
LoginHint is given, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is true, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person types the password
  4. Verify the password: the password is right: the subject is fixed and pwd joins the amr
  5. Read the account the password proved: the account is read with every factor
  6. Apply the downgrade rule, then offer a passkey (rule 8): the transport does not serve webauthn_get, or every passkey of the account is disabled: no step the transport serves can complete the sign-in here
Ask for the password and the identifier
ends here
LoginHint is absent, servesGet is false, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport does not serve webauthn_get: the preferred passkey gives way to the password
Go on with the password or the identifier
ends here
LoginHint is absent, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: no login_hint: the discoverable passkey, with the password and the identifier offered
  3. Ask for a passkey, with the password and the identifier offered: the person answers the password or the identifier instead
Ask for the password again
ends here
LoginHint is given, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person types the password
  4. Verify the password: a wrong password, or none to verify: the password step again, the same for every account
Ask for the passkey again
ends here
LoginHint is absent, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: no login_hint: the discoverable passkey, with the password and the identifier offered
  3. Ask for a passkey, with the password and the identifier offered: the person taps a passkey
  4. Reserve the assertion on the limiters: the round is reserved, and counted as a failure until it settles
  5. Take the ceremony and verify the assertion: no UV flag in the signed authenticator data: refused as invalid_credentials
Offer a passkey again
ends here
LoginHint is given, servesGet is true, hostedPages is true, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person types the password
  4. Verify the password: the password is right: the subject is fixed and pwd joins the amr
  5. Read the account the password proved: the account is read with every factor
  6. Apply the downgrade rule, then offer a passkey (rule 8): a password-only sign-in on the hosted pages: a passkey is offered
  7. Start, or keep, the transaction's registration ceremony: the ceremony is held
  8. Offer a passkey: the person creates a passkey
  9. Take the ceremony and verify the new passkey: refused as invalid_credentials, with a new ceremony
Refuse the round for a limit
ends here
LoginHint is absent, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: no login_hint: the discoverable passkey, with the password and the identifier offered
  3. Ask for a passkey, with the password and the identifier offered: the person taps a passkey
  4. Reserve the assertion on the limiters: a delay or a hard limit refuses the round: nothing is verified
End the transaction
ends here
LoginHint is given, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: a login_hint: the password, with the passkey offered
  3. Ask for the password, with a passkey offered: the person taps a passkey
  4. Reserve the assertion on the limiters: the round is reserved, and counted as a failure until it settles
  5. Take the ceremony and verify the assertion: another account's passkey: the transaction ends, audited as subject_mismatch
Refuse a malformed answer
ends here
LoginHint is absent, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: no login_hint: the discoverable passkey, with the password and the identifier offered
  3. Ask for a passkey, with the password and the identifier offered: the answer is not a JSON assertion of at most 8192 characters: refused as invalid_answer
Refuse the round while a store is down
ends here
LoginHint is absent, servesGet is true, hostedPages is false, the limiters let the round through, a passkey of the realm, not disabled, signed this round's challenge for the realm's RP ID from an allowed origin, with user presence and user verification, the transaction names no account yet, or the passkey is that account's, accountEnabled is false, password is right, passkeyBound is false, passkeyUsable is false, at most 5 minutes, a new passkey for this round's challenge and the realm's RP ID, from an allowed origin, discoverable, with user verification, an allowed algorithm and attestation "none", mailboxProved is false
  1. Choose the first response (rules 6 and 7): the transport serves webauthn_get
  2. Start, or keep, the transaction's assertion ceremony: it fails, because the challenge store fails; nothing is offered

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

What is guaranteed

These hold in all 125520 situations the model can reach, from 18432 different starting situations.

Exact rules
IdNever
the-first-response-reads-no-accountstate in ['offeringPasskeyFirst', 'offeringPasswordFirst', 'passwordOnlyOffer'] and accountRead
reserve-before-verifyassertionChecked and not attemptReserved
a-ceremony-is-taken-before-it-is-checkedassertionChecked and not ceremonyTaken
user-verification-or-nothingpasskeyProved and assertion != 'verified'
the-downgrade-rulestate == 'completed' and passkeyBound and not passkeyProved
a-mailbox-before-the-first-passkeypasskeyStored and not mailboxProved
a-fresh-factor-before-a-bindingpasskeyStored and authAge == 'stale'
every-binding-is-toldpasskeyStored and not bindingNoticed
never-a-key-storekeyStoreClaimed
every-refusal-is-auditedstate in ['assertionRefused', 'registrationRefused', 'throttled', 'answerRefused'] and not audited
every-round-waits-the-floorstate in ['offeringPasskeyFirst', 'offeringPasswordFirst', 'offeringPasskey', 'offeringEnrolment', 'completed', 'passkeyUnavailable', 'passwordOnlyOffer', 'passwordRefused', 'assertionRefused', 'registrationRefused', 'throttled', 'ended', 'answerRefused', 'unavailable'] and not floorWaited

Reference

All transitions (60)
FromEventGuardToEffectsTaken
firstRoundrule 1notServedpasswordOnlyOfferwaitFlooryes
firstRoundrule 2startingCeremonyyes
startingCeremonyDONEhintedofferingPasswordFirstwaitFlooryes
startingCeremonyDONEofferingPasskeyFirstwaitFlooryes
startingCeremonyFAIL.storeunavailablewaitFlooryes
reservingAssertionDONEclosedthrottledaudit waitFlooryes
reservingAssertionDONEverifyingAssertionreserveAttemptyes
reservingAssertionFAIL.storeunavailablewaitFlooryes
verifyingAssertionDONEwithoutUvassertionRefusedtakeCeremony checkAssertion audit waitFlooryes
verifyingAssertionDONEbadAssertionassertionRefusedtakeCeremony checkAssertion audit waitFlooryes
verifyingAssertionDONEmismatchendedtakeCeremony checkAssertion audit waitFlooryes
verifyingAssertionDONEreadingAccounttakeCeremony checkAssertionyes
verifyingAssertionFAIL.storeunavailabletakeCeremony waitFlooryes
readingAccountDONEdisabledBeforeFactorassertionRefusedreadAccount audit waitFlooryes
readingAccountDONEdisabledAfterFactorendedreadAccount audit waitFlooryes
readingAccountDONEafterFirstFactorreadAccount provePasskey audityes
readingAccountFAIL.storeunavailablewaitFlooryes
verifyingPasswordDONEwrongPasswordpasswordRefusedaudit waitFlooryes
verifyingPasswordDONEreadingProvenAccountprovePasswordyes
verifyingPasswordFAIL.storeunavailablewaitFlooryes
readingProvenAccountDONEafterFirstFactorreadAccountyes
readingProvenAccountFAIL.storeunavailablewaitFlooryes
afterFirstFactorrule 1passkeyOwed and passkeyAskablestartingDowngradeyes
afterFirstFactorrule 2passkeyOwedpasskeyUnavailablewaitFlooryes
afterFirstFactorrule 3enrolmentOfferedstartingRegistrationyes
afterFirstFactorrule 4completedwaitFlooryes
startingDowngradeDONEofferingPasskeywaitFlooryes
startingDowngradeFAIL.storeunavailablewaitFlooryes
startingRegistrationDONEofferingEnrolmentwaitFlooryes
startingRegistrationFAIL.storecompleteddeclineEnrolment waitFlooryes
verifyingRegistrationDONEbadRegistrationregistrationRefusedaudit waitFlooryes
verifyingRegistrationDONEcheckingBindingyes
verifyingRegistrationFAIL.storeunavailablewaitFlooryes
checkingBindingrule 1mailboxOwedprovingMailboxyes
checkingBindingrule 2stalecompleteddeclineEnrolment waitFlooryes
checkingBindingrule 3storingPasskeyyes
provingMailboxDONEcheckingBindingproveMailbox timePassesyes
provingMailboxFAIL.throttledthrottledaudit waitFlooryes
provingMailboxFAIL.endedendedwaitFlooryes
provingMailboxFAIL.invalidanswerRefusedaudit waitFlooryes
provingMailboxFAIL.storeunavailablewaitFlooryes
storingPasskeyDONEowingRecoveryCodesstorePasskey audityes
storingPasskeyFAIL.boundregistrationRefusedaudit waitFlooryes
storingPasskeyFAIL.endedendedaudit waitFlooryes
storingPasskeyFAIL.storeunavailablewaitFlooryes
offeringPasskeyFirstROUNDstartingCeremonynewRoundyes
offeringPasskeyFirstASSERTIONreservingAssertionnewRound moveOnyes
offeringPasskeyFirstOTHERotherStepAnswerednewRoundyes
offeringPasskeyFirstMALFORMEDanswerRefusednewRound audit waitFlooryes
offeringPasswordFirstROUNDstartingCeremonynewRoundyes
offeringPasswordFirstASSERTIONreservingAssertionnewRound moveOnyes
offeringPasswordFirstPASSWORDverifyingPasswordnewRound moveOnyes
offeringPasswordFirstMALFORMEDanswerRefusednewRound audit waitFlooryes
offeringPasskeyROUNDstartingDowngradenewRoundyes
offeringPasskeyASSERTIONreservingAssertionnewRound moveOnyes
offeringPasskeyMALFORMEDanswerRefusednewRound audit waitFlooryes
offeringEnrolmentROUNDstartingRegistrationnewRoundyes
offeringEnrolmentDECLINEcompletednewRound declineEnrolment waitFlooryes
offeringEnrolmentREGISTRATIONverifyingRegistrationnewRound answerRegistrationyes
offeringEnrolmentMALFORMEDanswerRefusednewRound audit waitFlooryes