This guide details how to automatically and silently register a passkey for a user immediately after a successful password-based sign-in, minimizing friction and boosting passkey adoption.
The Right Trigger Moment
Automatic passkey creation (also known as Conditional Create or silent post-login promotion) MUST only be triggered immediately after a successful, full sign-in that involved a password.
- Do not attempt conditional creation for passwordless flows (e.g., magic links, SMS OTP, or identity federation).
- If multi-factor authentication is required, you MUST wait until all factors have succeeded before initiating conditional creation.
- Ensure a valid, authenticated user session is active before making requests to creation endpoints.
Implementation Steps
1. Abort Prior Autofill Actions
If the sign-in page utilizes form autofill (Conditional UI/Get), the active credential get call must be aborted to prevent browser conflicts.
- Call
abortController.abort()on theAbortControllerattached to the pendingnavigator.credentials.get()autofill request before callingnavigator.credentials.create().
2. Feature Detection
Determine whether Conditional Create is available by checking conditionalCreate with PublicKeyCredential.getClientCapabilities().
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate) {
// Conditional create is available
}
3. Create a passkey with Conditional Create
- Pass
mediation: 'conditional'within thenavigator.credentials.create()options. This signals the browser to handle the passkey creation flow silently in the background or contextually without throwing obtrusive modal dialogs. - Populate
excludeCredentialswith the user's existing passkey credential IDs to avoid registering duplicate keys.
4. Silent Error Handling
- Wrap the passkey creation prompt (
navigator.credentials.create) in a try/catch block. You MUST catch and silently ignore typical user-facing exceptions (InvalidStateError,NotAllowedError,AbortError) without rendering any error UI to the user.
5. Server-Side User Presence Verification
- The server-side verification endpoint MUST relax the User Presence (UP) requirement (
requireUserPresence: false) ONLY when verifying credentials produced by a conditional-create trigger. Strict presence verification must remain active for standard explicit creations.
6. Handle Failed Server Verification gracefully
- If
navigator.credentials.create()succeeds but the server verification fetch returns a bad response (e.g., signature verification fails), invokePublicKeyCredential.signalUnknownCredential()to prevent orphaned credentials from lingering in the passkey provider.
Code Example
// optionsFetch and registerVerifyFetch are app-defined server endpoint requests
import { optionsFetch, registerVerifyFetch } from './api.js';
async function triggerConditionalCreate(loginAbortController) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate !== true) {
return; // Platform does not support conditional creation
}
// 1. Abort any active autofill conditional-get controllers to clear the WebAuthn pipeline
loginAbortController.abort();
// 2. Fetch creation options signaling the backend that this is a conditional request
const creationOptionsJSON = await optionsFetch({ conditional: true });
const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential;
try {
// 3. Invoke silent credentials creation prompt
credential = await navigator.credentials.create({
publicKey,
mediation: 'conditional' // Silent background creation mediation
});
} catch (e) {
// 4. Silently swallow common WebAuthn browser exceptions
if (['InvalidStateError', 'NotAllowedError', 'AbortError'].includes(e.name)) {
return;
}
console.error('Unexpected conditional create error:', e);
return;
}
// 5. Server verification step using dedicated Try/Catch block
let encodedResponse = credential.toJSON();
try {
const response = await registerVerifyFetch(encodedResponse);
if (!response.ok) {
// If the server verification fails, clean up using Signal API
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id
});
}
}
} catch (serverErr) {
console.error('Verification network failure:', serverErr);
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id
});
}
}
}
Fallback Strategies
Passkey feature detection fallback
Always install 'webauthn-polyfills' and import it in the context.
Consider as long as PublicKeyCredential is supported, PublicKeyCredential.getClientCapabilities is also supported.
import 'webauthn-polyfills';
Signal API Synchronization Fallback
The WebAuthn Signal API (webauthn-signals) is a progressive optimization used to keep password managers in sync with the server credential state.
- Fallback Experience: Gated via
if (PublicKeyCredential.signalUnknownCredential). If unsupported, the background verification sync is bypassed gracefully without throwing browser exceptions.
Easy JSON Serialization Fallback
Always install 'webauthn-polyfills' and import it in the context.
Consider as long as PublicKeyCredential is supported, PublicKeyCredential.parseCreationOptionsFromJSON and PublicKeyCredential.prototype.toJSON are also supported.
import 'webauthn-polyfills';