With an empty allowCredentials list, how does your server work out which account is signing in?
answer
- no username typed yet
- empty allow list, discoverable credential
- userHandle resolves the account
- residentKey asked for at registration
- no per-account throttle before verification
basics
~20 sAn empty allowCredentials list means the server has not identified anyone yet. The authenticator offers a discoverable credential it holds for that rpId, and the assertion comes back carrying a userHandle the server matches to the account it issued that handle to.
solid answer
~40 sUsername-less sign-in is set up at registration, not at sign-in: the credential must have been created with `residentKey` required, so the authenticator stores it discoverably and can offer it unprompted. At sign-in you issue options with an empty `allowCredentials`, the same options for everyone, and the response carries both a credential id and a `userHandle`. Resolve the credential record by its id, confirm the record's stored `userHandle` equals the returned one, and only then run the ordinary checks - ceremony type, challenge, `origin`, `rpIdHash`, flags, signature - against that record's public key. The trade is real: no account is known before verification, so any per-account throttling has to key on something else.
code
json · 7 lines{
"challenge": "Rk0xbTdZdEwzcVAyc0Q4dg",
"rpId": "quota-reports.example",
"allowCredentials": [],
"userVerification": "required",
"timeout": 120000
}go deeper
Remember the two pieces: the credential has to have been stored discoverably at registration, and the assertion comes back carrying a user handle that the server maps to an account.
Explain why the resolution happens after the response rather than before, and why the returned handle must be checked against the handle stored on the credential record rather than trusted on its own.
Discuss what the design costs: no account is known before verification, so per-account throttling cannot be the first gate; authenticator storage slots are finite; and a handle that resolves to nothing is a failure, never a signup.
Weigh uniformity against control. Identical options for every caller means the ceremony confirms nothing about which accounts exist, and that property is worth the abuse controls you have to rebuild around a credential you have not yet resolved.
## What discoverable means A credential is **discoverable** when the authenticator itself stores enough to find it - the credential id, the relying-party identifier and the `userHandle` - rather than needing the server to hand it a list of ids to look for. That is what allows a skipper to walk up to the quota return on a shared wheelhouse tablet, tap a roaming key, and be signed in without typing anything at all. The decision is made at registration. `residentKey` required in the creation options asks the authenticator to store the credential discoverably, and an authenticator with no free slots will say so then rather than at sign-in. A credential registered without it can still be used, but only when your server names its id in `allowCredentials`, which means the user has to identify themselves first. ## The asymmetry between the two ceremonies - **Registration must ask.** Discoverability is a property of what the authenticator stores, so it is requested when the credential is created and cannot be added afterwards. - **Authentication must resolve.** With an empty `allowCredentials`, the server issues identical options to every caller and finds out who it is talking to only from the response. ## What the server does, step by step 1. Issue request options with a fresh challenge, the `rpId`, an empty `allowCredentials` and your `userVerification` preference. These options are identical for everyone, which is why the ceremony reveals nothing about which accounts exist. 2. Receive the assertion. It carries a credential id, `authenticatorData`, `clientDataJSON`, a signature, and a `userHandle`. 3. Look up the credential record by its id. If there is no such record, the ceremony fails - an unknown credential id is not a user you have. 4. Confirm the record's stored `userHandle` equals the returned one. This is the step people skip. The returned handle is data from the client; the record is the thing you wrote at registration, and the two must agree. 5. Run the ordinary verification - ceremony type, challenge, `origin`, `rpIdHash`, flags, signature under that record's public key. 6. Only now do you have an account. ## What you give up | with a username first | with an empty allow list | |---|---| | the account is known before any ceremony runs | no account is known until verification completes | | per-account throttling can run before verification | throttling must key on the caller or the credential instead | | you can offer only that account's credentials | the authenticator decides which of its credentials to offer | | a non-discoverable credential works fine | the credential must have been registered discoverably | | typing is required, and on a wet quayside that matters | nothing is typed at all | The throttling row is the one worth saying out loud. Until the signature verifies you have no account to count failures against, so a per-account attempt counter cannot be the first line - and an unknown credential id, which is the cheapest thing for an attacker to send, never resolves to an account at all. What you gain in exchange is genuinely valuable: the ceremony is uniform, so it confirms nothing about whether any particular account exists. ## Practical notes people miss - **Slots are finite.** Roaming authenticators hold a limited number of discoverable credentials and can refuse a new one. Handle that refusal at registration with a comprehensible message, not a stack trace. - **The label in the picker comes from registration.** The account name and display name you put in the creation options are what the client shows later. The `userHandle` is not that label and must not be made readable to serve as one. - **A returned handle for an account you cannot find is a failure, not a signup.** Auto-creating an account from an unrecognised handle turns a sign-in into an account-creation path nobody designed. - **Requiring user verification is a separate choice.** Discoverability decides whether the credential can be found without a username; `userVerification` decides whether a local gesture was demanded. Asking for both is what makes one tap stand on its own. - **The shared tablet is the case to think through.** Several skippers may hold discoverable credentials on the same wheelhouse device, so the client shows a chooser and your server must be indifferent to which entry is picked - it finds out from the response, and the only account it signs in is the one the resolved credential record names. - **Offer the other route as well.** Keeping a username-first path alive costs little and covers the authenticator whose slots were full at registration, so a skipper is never stranded by a storage limit they cannot see.
- The assertion carries a user handle for an account you cannot find. What now?Fail the ceremony. A handle you did not issue, or one whose account has gone, resolves to nothing - and creating an account on the strength of it turns sign-in into an unplanned registration path. Return the same outcome you return for any other failed sign-in, so the response says nothing about which accounts exist.
- If no account is known before verification, where does throttling attach?To whatever you do know: the caller, the credential id once it resolves, and the overall rate of failing ceremonies. Per-account counting still has a place after the credential resolves, but it cannot be the first gate, and the cheapest attack - an unrecognised credential id - never reaches an account at all.
saying these in an interview costs you the question
- Looks up the account from the rpIdHash instead of the user handle
- Believes an empty allow list means any credential will be accepted
- Assumes username-less sign-in works without a discoverable credential
- Assumes per-account throttling can run before the credential resolves
- Trusts the returned user handle without matching the stored credential record
- Creates an account when the returned handle is unrecognised