Security and resilience

Passkey Authentication: Production-Ready Patterns in 2026

How passwordless authentication with WebAuthn and passkeys evolved from experiment to production standard.

3/12/20267 min readSecurity
Passkey Authentication: Production-Ready Patterns in 2026

Executive summary

How passwordless authentication with WebAuthn and passkeys evolved from experiment to production standard.

Last updated: 3/12/2026

Introduction: The end of the password era in production

Password-based authentication is dead in production. In 2026, passkeys — implemented via the WebAuthn API — have become the de facto standard for new projects, replacing both traditional passwords and SMS-based 2FA for most use cases.

The technology isn't new. FIDO2 and WebAuthn have existed since 2018. What changed in 2026 is platform support maturity: all modern browsers implement WebAuthn natively, iOS and Android have first-class support for passkeys, and identity providers like Google, Apple, and Microsoft offer sync-on-cloud implementations.

For software engineers, the question is no longer "if to implement passkeys", but "how to implement them in a way that works in production, respects the user experience, and handles real edge cases".

WebAuthn Architecture: What You Need to Understand

WebAuthn defines a browser API for registering and authenticating public key credentials. The key technical detail is that the private key never leaves the user's device — it remains in the TPM (Windows), Secure Enclave (macOS/iOS), or TEE (Android).

Registration Flow

typescript// 1. Generate challenge on server
const registrationOptions = await generateRegistrationOptions({
  rpName: 'My Application',
  rpID: window.location.hostname,
  userID: user.id,
  userName: user.email,
  excludeCredentials: user.existingCredentials.map(c => ({
    id: c.credentialId,
    type: 'public-key',
  })),
});

// 2. Call WebAuthn in browser
const registration = await startRegistration(registrationOptions);

// 3. Verify on server
const verification = await verifyRegistrationResponse({
  response: registration,
  expectedChallenge: registrationOptions.challenge,
  expectedOrigin: window.location.origin,
  expectedRPID: window.location.hostname,
});

// 4. Store credential
await db.credentials.insert({
  credentialId: verification.registrationInfo.credentialId,
  publicKey: verification.registrationInfo.credentialPublicKey,
  counter: verification.registrationInfo.counter,
  transports: registration.response.transports,
  backupEligible: verification.registrationInfo.credentialDeviceType === 'singleDevice',
  backupState: verification.registrationInfo.credentialBackedUp,
});

Authentication Flow

typescript// 1. Generate challenge on server
const authOptions = await generateAuthenticationOptions({
  rpID: window.location.hostname,
  userVerification: 'preferred',
  allowCredentials: user.credentials.map(c => ({
    id: c.credentialId,
    type: 'public-key',
    transports: c.transports,
  })),
});

// 2. Call WebAuthn in browser
const authentication = await startAuthentication(authOptions);

// 3. Verify on server
const verification = await verifyAuthenticationResponse({
  response: authentication,
  expectedChallenge: authOptions.challenge,
  expectedOrigin: window.location.origin,
  expectedRPID: window.location.hostname,
  authenticator: {
    credentialID: userCredential.credentialId,
    credentialPublicKey: userCredential.publicKey,
    counter: userCredential.counter,
    transports: userCredential.transports,
  },
});

// 4. Update counter and create session
await db.credentials.update(verification.authenticationInfo.newCounter);
await createSession(user.id);

Production Implementation Patterns

Passkey-First with Fallback

The most robust approach is to implement passkeys as the primary authentication method while maintaining fallback options for gradual transition:

typescriptasync function handleLogin(username: string) {
  const user = await db.users.findByUsername(username);

  if (!user.hasPasskey) {
    // Fallback: passwords or magic links during transition
    return renderPasswordOrMagicLinkForm();
  }

  if (user.passkeyCount === 1 && !user.passkeyBackupState) {
    // Warning: user has passkey without backup
    return renderSinglePasskeyWarning();
  }

  // Display WebAuthn prompt
  return triggerWebAuthnAuthentication(user);
}

Multi-Device Synchronization

The critical UX decision is whether you require sync-on-cloud passkeys (Apple Keychain, Google Password Manager) or allow device-only passkeys:

Sync-on-cloud passkeys:

  • User can login from any device logged into account
  • Automatic backup if device is lost
  • Requires platform ecosystem (Apple/Google/Windows)

Device-only passkeys:

  • Works without platform ecosystems
  • Higher security (credential never leaves device)
  • Lockout risk if device is lost
typescript// Detect during registration
const isSyncEligible = registration.response.transports.includes('internal');

await db.credentials.update({
  backupEligible: isSyncEligible,
  backupState: registration.response.clientExtensionResults?.credProps?.rk,
});

// Force backup for production
if (isSyncEligible && !backupState) {
  return renderBackupRequiredWarning();
}

Managing Multiple Credentials

Productive users accumulate multiple passkeys over time. Your implementation should handle this:

typescriptasync function listUserCredentials(userId: string) {
  const credentials = await db.credentials.findByUserId(userId);

  return credentials.map(cred => ({
    id: cred.credentialId,
    name: formatCredentialName(cred), // "iPhone 15 Pro - September 2026"
    lastUsed: cred.lastUsedAt,
    isCurrentDevice: isCurrentDevice(cred),
    hasBackup: cred.backupState,
  }));
}

async function removeCredential(credentialId: string) {
  const user = await db.users.findByCredentialId(credentialId);

  if (user.credentials.length === 1) {
    throw new Error('Cannot remove last credential without login alternative');
  }

  await db.credentials.delete(credentialId);
}

Critical Security Considerations

User Verification: When is it Required?

userVerification in WebAuthn controls whether the device must biometrically (FaceID, TouchID, Windows Hello) or via PIN verify the user:

typescript// For sensitive operations, require UV
const sensitiveAuthOptions = generateAuthenticationOptions({
  userVerification: 'required', // Biometrics or PIN required
});

// For standard login, 'preferred' allows smoother UX
const standardAuthOptions = generateAuthenticationOptions({
  userVerification: 'preferred', // Biometrics if available, but not required
});

Protection Against Replay Attacks

Each authentication response includes a counter that must be incremented and verified:

typescriptasync function verifyAuthenticationResponse(response) {
  const verification = await verify(authentication);

  // Verify counter to prevent replay attacks
  if (verification.authenticationInfo.newCounter <= authenticator.counter) {
    throw new Error('Counter was not incremented - possible replay attack');
  }

  return verification;
}

Rate Limiting WebAuthn

WebAuthn operations are expensive (require user interaction). Implement aggressive rate limiting:

typescriptasync function checkRateLimit(identifier: string) {
  const attempts = await rateLimiter.get(identifier);

  if (attempts > 3) {
    throw new Error('Too many attempts. Try again in 5 minutes.');
  }

  await rateLimiter.increment(identifier);
}

Production UX Patterns

Contextual Biometric Prompt

WebAuthn authentication UX depends on clear contextual messages:

// GOOD: specific and actionable
"Touch the fingerprint reader to continue in MyApp"
"Insert your USB security key"

// BAD: generic
"Authenticate"
"Verify identity"

Intelligent Error Feedback

WebAuthn errors are cryptic by default. Translate to user messages:

typescriptfunction translateWebAuthnError(error: Error): string {
  if (error.name === 'NotAllowedError') {
    return 'Operation cancelled or timeout. Please try again.';
  }

  if (error.name === 'NotSupportedError') {
    return 'Your device does not support WebAuthn authentication.';
  }

  if (error.name === 'SecurityError') {
    return 'Domain not allowed or security error.';
  }

  return 'Authentication error. Please contact support.';
}

Migration Strategy for Existing Systems

Migrating from passwords to passkeys requires strategy, not a switch:

Phase 1: Passkey as Additional Option

  • Users can register passkeys
  • Password remains primary method
  • Collect adoption metrics

Phase 2: Passkey-First with Fallback

  • Passkey appears first in UI
  • Password available as secondary option
  • Measure fallback usage rate

Phase 3: Passkey-Only

  • Passkeys as only method
  • Account-based recovery for lockouts
  • Gradually deprecate passwords
typescriptasync function determineAuthMethod(user: User) {
  const passkeyCount = await db.credentials.countByUser(user.id);

  if (passkeyCount > 0 && !user.requiresPassword) {
    return { method: 'passkey', fallback: 'password' };
  }

  if (passkeyCount === 0) {
    return { method: 'password', suggestion: 'enable-passkey' };
  }

  return { method: 'passkey', fallback: null };
}

When Passkeys Are Not Appropriate

Passkeys are not a universal solution. Consider alternatives when:

  • You need programmatic authentication (API keys, JWT)
  • Your audience uses very old legacy browsers
  • You operate in high-security environments with specific requirements
  • Your application is headless or command-line based

In these cases, combine passkeys with other methods or maintain authentication alternatives.

Conclusion

Passkey authentication in 2026 is an implementation question, not a technology question. The WebAuthn API is stable, platform support is ubiquitous, and production patterns are well-defined.

The real decision is about UX: how you present passkeys, how you manage multiple devices, how you handle synchronization edge cases, and how you migrate from existing systems without causing friction.

For teams implementing authentication today, the question is not "should I use passkeys", but "how do I transition in a way that improves security without diminishing user experience".


Need to implement passkey authentication in production with security and UX considerations? Talk to Imperialis web specialists to design and implement modern authentication.

Sources

Related reading