Discoverable vs Server-Side Credentials
Whether a WebAuthn credential is discoverable (a resident key the authenticator can enumerate on its own) or server-side (a non-resident key the RP must name via allowCredentials) is the single decision that determines whether usernameless, one-tap passkey sign-in is even possible. It also drives authenticator storage cost, the shape of your authentication request, and your fallback UX. This page is the decision reference, grounded in WebAuthn Level 2 §5.4.7 and CTAP2. It expands the storage trade-off introduced in public key vs symmetric credential types and sits under WebAuthn & FIDO2 Protocol Fundamentals.
Concept Definition and Spec Grounding
The controlling knob is residentKey inside authenticatorSelection (WebAuthn L2 §5.4.4), which supersedes the legacy boolean requireResidentKey:
residentKey: 'required'→ a discoverable credential. The authenticator stores the credential and the associateduserHandleand RP ID in its own memory, so it can list matching credentials without a hint. This is what makes a passkey appear in conditional mediation autofill with no username typed.residentKey: 'discouraged'→ a server-side (non-resident) credential. The authenticator derives the key from a wrapped key handle stored in the returnedcredentialId; it keeps nothing locally, so the RP must supply thecredentialIdinallowCredentialsfor the user to authenticate.residentKey: 'preferred'→ discoverable if the authenticator can, otherwise server-side.
Discoverability is also surfaced post-registration: the authenticator sets the BE/BS flags and reports credential properties (credProps.rk) telling you whether a resident key was actually created.
Architecture and Data Flow
The flow below contrasts the two authentication ceremonies: usernameless (empty allowCredentials) versus username-first (server supplies the list).
Implementation Guide
Step 1 — Choose the residentKey policy at registration
// Discoverable: enables usernameless autofill sign-in
authenticatorSelection: { residentKey: 'required', userVerification: 'preferred' }
// Server-side: no resident slot consumed on the authenticator
authenticatorSelection: { residentKey: 'discouraged', userVerification: 'preferred' }
Step 2 — Confirm what was actually created (credProps)
Request the credProps extension and read rk to know whether a resident key was stored — authenticators may downgrade preferred.
// client: extensions: { credProps: true }
const ext = credential.getClientExtensionResults();
const isDiscoverable = ext.credProps?.rk === true;
await saveCredential({ /* … */, discoverable: isDiscoverable });
Step 3 — Shape the authentication request accordingly
// Usernameless (discoverable): empty allowCredentials → any resident passkey
const options = await generateAuthenticationOptions({ rpID, allowCredentials: [], userVerification: 'preferred' });
// Username-first (server-side): supply the stored credential IDs
const creds = await lookupCredentials(username); // from your indexed store
const options2 = await generateAuthenticationOptions({
rpID,
allowCredentials: creds.map((c) => ({ id: c.credentialId, type: 'public-key', transports: c.transports })),
});
The lookup that feeds allowCredentials is exactly the concern covered in credential indexing and database schema design.
Step 4 — Resolve the account from userHandle (discoverable only)
For usernameless sign-in the RP identifies the user from the returned userHandle, not a typed username, so userHandle must be a stable, non-PII account identifier set at registration.
const userHandle = base64url(assertion.response.userHandle); // set to an opaque account id at registration
const account = await findAccountByUserHandle(userHandle);
Validation Checklist
Error Reference Table
| Error / Condition | HTTP Status | Trigger | Diagnostic |
|---|---|---|---|
ConstraintError (client) |
— | residentKey: 'required' but authenticator storage full |
Inform user; allow server-side fallback |
| Empty autofill dropdown | — | Credential is server-side, not discoverable | Check credProps.rk; re-register as discoverable |
| No account for userHandle | 404 | userHandle not stored or mismatched |
Verify userHandle set at registration equals stored id |
| Assertion with no allowCredentials fails | 400 | Server-side credential expected but none named | Populate allowCredentials for username-first flow |
| PII leaked in userHandle | — (privacy) | Username/email used as userHandle |
Replace with opaque id; re-enrol |
Platform and Library Notes
@simplewebauthn/server
generateRegistrationOptions({ authenticatorSelection: { residentKey } }) and the credProps extension are supported; verifyAuthenticationResponse exposes userHandle for account resolution. Empty allowCredentials yields the usernameless flow.
Roaming security keys
Resident-key slots are finite (older keys hold ~25); mandating required across many RPs can exhaust them. Prefer preferred for broad hardware-key support.
iOS / Android platform passkeys
Synced passkeys are always discoverable and effectively unlimited, so required is safe. userHandle round-trips through the sync.
Windows Hello
Discoverable credentials are supported but device-bound (no sync); plan recovery accordingly, and see platform vs roaming trade-offs.
Pitfalls and Security Hardening
1. Expecting autofill from server-side credentials. Root cause: residentKey: 'discouraged'. Mitigation: require discoverable credentials for usernameless sign-in.
2. Using PII as userHandle. Root cause: setting user.id to an email. Mitigation: use an opaque, stable identifier (WebAuthn L2 §5.4.3 recommends ≤64 random bytes).
3. Exhausting roaming-key resident slots. Root cause: required on hardware keys. Mitigation: use preferred; detect via credProps.rk.
4. No username-first fallback. Root cause: assuming every device holds a resident key. Mitigation: keep the allowCredentials path available.
5. Trusting preferred to always create a resident key. Root cause: not reading credProps. Mitigation: store rk and branch on it.
Related
- WebAuthn & FIDO2 Protocol Fundamentals — the parent pillar and credential model
- Public Key vs Symmetric Credential Types — the underlying resident-vs-server-side storage model
- When to Use Resident vs Discoverable Credentials — the focused decision framing for choosing the target state
- Migrating from Non-Discoverable to Discoverable Credentials — upgrading an existing server-side base to autofill
- Conditional Mediation and Passkey Autofill UI — why only discoverable credentials drive autofill