When to Use Resident vs Discoverable Credentials

Direct resolution of credential type selection for WebAuthn implementations requires clarifying a common architectural misconception: in modern FIDO2 specifications, resident and discoverable credentials are functionally synonymous. The distinction exists solely in API versioning and authenticator capability flags. Selecting the correct configuration dictates whether your relying party (RP) can support username-less authentication, how credential IDs are routed during assertion, and whether your compliance posture aligns with current CTAP2.1 requirements.

Spec Mapping & API Evolution (WebAuthn Level 2 vs Level 3)

The W3C specification transitioned from residentKey (Level 2) to discoverableCredentials (Level 3) to strictly align with CTAP2.1 authenticator behavior. Misalignment between browser implementations and authenticator firmware triggers silent fallbacks to non-resident flows, breaking username-less UX expectations. Understanding how these flags map to actual key storage behavior requires a baseline grasp of Public Key vs Symmetric Credential Types.

Diagnostic & Error Resolution

Symptom Error Code Root Cause Remediation
Registration fails immediately TypeError: Failed to execute 'create' on 'PublicKeyCredential': Invalid residentKey requirement Legacy residentKey: 'required' passed to browsers expecting discoverableCredentials: true Normalize payload using feature detection before invoking navigator.credentials.create()
Silent downgrade to non-resident No explicit error; credential ID returned in registration response Authenticator firmware mismatch (CTAP2.0 vs CTAP2.1) or missing discoverableCredentials flag Map residentKey enum to boolean discoverableCredentials for cross-browser compatibility

Diagnostic Command:

// Inspect resolved authenticator selection in browser DevTools console
const resolved = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
console.log('Platform supports resident keys:', resolved);

Production Patch:

// Dual-flag fallback for cross-version compatibility
const options = {
 publicKey: {
 rp: { name: 'Example Corp', id: 'example.com' },
 user: { id: Uint8Array.from('user123', c => c.charCodeAt(0)), name: '[email protected]', displayName: 'User' },
 challenge: crypto.getRandomValues(new Uint8Array(32)),
 pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
 authenticatorSelection: {
 discoverableCredentials: true, // WebAuthn L3 / CTAP2.1
 residentKey: 'required', // WebAuthn L2 fallback
 userVerification: 'required'
 }
 }
};

Debugging NotAllowedError & InvalidStateError During Discovery

When discoverableCredentials is enabled but the authenticator lacks secure element capacity or violates platform constraints, the browser returns NotAllowedError or InvalidStateError. This typically occurs when the RP requests credential discovery without verifying authenticatorAttachment or userVerification constraints, or attempts duplicate registration.

Diagnostic & Error Resolution

Symptom Error Code Root Cause Remediation
Discovery prompt dismissed or blocked NotAllowedError (DOMException) RP requests discovery on a roaming authenticator without cross-device sync capability Implement conditional UI flow checking platform availability before invoking discovery
Registration collision InvalidStateError: The credential already exists Duplicate credential registration attempt without prior get() assertion check Query existing credentials via navigator.credentials.get() before create() to prevent collision

Diagnostic Command:

# Browser DevTools Console: Verify conditional mediation support
PublicKeyCredential.isConditionalMediationAvailable().then(console.log);
// Returns true if autofill/discovery UI can be injected natively

Production Patch:

async function safeRegister(publicKeyOptions) {
 try {
 // Pre-flight check to prevent InvalidStateError collisions
 await navigator.credentials.get({ publicKey: { ...publicKeyOptions, allowCredentials: [] } });
 } catch (err) {
 if (err.name === 'InvalidStateError') {
 console.warn('Credential already provisioned. Trigger assertion flow instead.');
 return handleExistingCredential();
 }
 if (err.name === 'NotAllowedError') {
 throw new Error('User verification or platform constraints blocked discovery.');
 }
 throw err;
 }
 return navigator.credentials.create({ publicKey: publicKeyOptions });
}

Configuration Matrix for Identity Platform Builders

Select credential type based on threat model, UX requirements, and storage architecture. Resident/discoverable credentials enable true username-less authentication but require secure element or platform-bound storage. Non-resident credentials rely on server-side credential ID mapping and explicit user identification during login.

Requirement Recommended Configuration Storage Behavior Compliance Impact
Username-less login discoverableCredentials: true Authenticator stores private key + user handle locally Requires FIDO2 Level 1+ certification; aligns with phishing-resistant MFA mandates
Cross-device sync (Passkeys) discoverableCredentials: true + userVerification: 'required' Cloud-synced secure enclave (iCloud Keychain, Google Password Manager) Meets NIST SP 800-63B AAL2/AAL3; audit trail required for credential provisioning
High-volume enterprise SSO discoverableCredentials: false RP stores credential ID in user directory; authenticator only holds private key Reduces authenticator storage limits; requires robust server-side credential routing

For complete protocol handshake validation and architectural alignment, reference WebAuthn & FIDO2 Protocol Fundamentals.

Step-by-Step Compliance & Migration Fix

Legacy implementations often hardcode residentKey: 'discouraged' or mix credential types within the same RP ID scope, triggering SecurityError during discovery. Audit existing registration payloads and enforce dynamic evaluation based on authenticatorSelection.requireResidentKey. Validate against FIDO2 certification logs to ensure CTAP2.1 compliance.

Diagnostic & Error Resolution

Symptom Error Code Root Cause Remediation
Assertion fails after migration SecurityError: The operation is insecure Mixed credential types in same RP ID scope Enforce consistent discoverableCredentials flag across all registration payloads
Discovery prompt fails silently NotAllowedError / SecurityError Missing userVerification flag during discovery request Enforce userVerification: 'required' for discoverable flows
Database collision on login IntegrityError / DuplicateKey Missing credential ID deduplication at RP layer Implement credential ID deduplication at the RP database layer

Production Patch:

// Dynamic configuration based on platform capability & compliance posture
const isDiscoverableSupported = 
 PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;

const residentConfig = isDiscoverableSupported 
 ? { discoverableCredentials: true, userVerification: 'required' }
 : { discoverableCredentials: false, userVerification: 'preferred' };

const registrationOptions = {
 publicKey: {
 rp: { id: window.location.hostname, name: 'SecureApp' },
 user: { id: new Uint8Array(16), name: '[email protected]', displayName: 'User' },
 challenge: crypto.getRandomValues(new Uint8Array(32)),
 pubKeyCredParams: [{ alg: -7, type: 'public-key' }, { alg: -257, type: 'public-key' }],
 authenticatorSelection: {
 ...residentConfig,
 residentKey: residentConfig.discoverableCredentials ? 'required' : 'discouraged'
 }
 }
};

Compliance Verification Steps:

  1. Run PublicKeyCredential.isConditionalMediationAvailable() to validate autofill compatibility.
  2. Inspect authenticatorData flags in registration response; verify rk (resident key) bit is set when discoverableCredentials: true.
  3. Cross-reference RP ID scope in your credential database; ensure no mixed residentKey/discoverableCredentials states exist for identical user handles.
  4. Validate against FIDO Alliance certification logs to confirm CTAP2.1 GetAssertion discovery routing matches your implementation.