Passwords have been the Achilles’ heel of digital security for decades. Despite countless breaches, credential stuffing attacks, and phishing campaigns that exploit password vulnerabilities, organizations continue to rely on this fundamentally flawed authentication method. In 2024, over 80% of data breaches still involve compromised credentials, with the average breach costing organizations $4.88 million.
WebAuthn (Web Authentication) represents a paradigm shift in how we approach online authentication. As a W3C standard developed in collaboration with the FIDO Alliance, WebAuthn enables websites and applications to implement strong, passwordless authentication using public-key cryptography. Instead of transmitting secrets that can be intercepted, WebAuthn uses cryptographic key pairs where the private key never leaves the user’s device.
This comprehensive guide answers the fundamental questions: what is WebAuthn, how does WebAuthn work, and provides practical guidance including how do I add WebAuthn passkeys to a dApp. Whether you’re a security professional evaluating authentication options, a developer implementing modern authentication, or an IT leader planning your organization’s passwordless journey, this guide provides the technical depth and practical insights you need.
What Is WebAuthn?
WebAuthn (Web Authentication API) is a web standard published by the World Wide Web Consortium (W3C) that enables strong authentication for web applications. It provides a JavaScript API that allows websites to create and use public-key credentials for user authentication, eliminating the need for passwords.
The Core Concept
At its heart, WebAuthn replaces the traditional “something you know” (passwords) with “something you have” (an authenticator device) combined with “something you are” or “something you know” (biometric verification or PIN on the device).
When you authenticate with WebAuthn:
- Your device generates a unique cryptographic key pair for each website
- The private key stays securely stored in hardware (never transmitted)
- The public key is shared with the website
- Authentication involves proving you possess the private key without revealing it
WebAuthn Within the FIDO2 Framework
WebAuthn is one component of the broader FIDO2 standard, which consists of two complementary specifications:
FIDO2 Standard Components:
WebAuthn (W3C Web Standard)
- Browser JavaScript API
- Server-side validation
- Credential management
CTAP (Client to Authenticator Protocol)
- USB communication
- NFC communication
- Bluetooth communication
- Platform authenticator interface
- WebAuthn: The browser-side API that web applications use
- CTAP (Client to Authenticator Protocol): How browsers communicate with authenticators
Key Terminology
Term | Definition |
Relying Party (RP) | The website or application implementing WebAuthn authentication |
Authenticator | Device that generates and stores credentials (security key, phone, laptop) |
Credential | The public/private key pair created for a specific relying party |
User Verification | Local authentication on the device (biometric, PIN) |
User Presence | Physical interaction proving a human is present (button press, tap) |
Attestation | Cryptographic proof of the authenticator’s identity and properties |
Passkey | Consumer-friendly term for WebAuthn credentials, especially synced ones |
Types of Authenticators
Platform Authenticators (Built-in)
- Windows Hello (fingerprint, face, PIN)
- Apple Touch ID / Face ID
- Android biometrics
- Integrated into the device, always available
Roaming Authenticators (External)
- USB security keys (YubiKey, Titan, Feitian)
- NFC security keys
- Bluetooth security keys
- Portable across devices
Hybrid Authenticators
- Smartphone as authenticator for computer login
- Cross-device authentication via QR code + Bluetooth
- Combines convenience with security
How Does WebAuthn Work?
Understanding how WebAuthn works requires examining its two core ceremonies: registration (creating credentials) and authentication (using credentials).
The Registration Ceremony
Registration occurs when a user first sets up WebAuthn authentication with a website.
Step-by-Step Flow:
Step-by-Step Registration Process:
- User → Clicks “Register”
- Browser → Requests registration options from server
- Server → Returns challenge + user info
- Browser → Calls navigator.credentials.create()
- Authenticator → Prompts user to verify identity (biometric/PIN)
- User → Approves registration
- Authenticator → Generates key pair, signs challenge
- Browser → Sends public key + attestation to server
- Server → Verifies and stores public key
- Server → Returns registration complete confirmation
Technical Details:
- Server generates options: Includes challenge (random bytes), relying party info, user info, and acceptable authenticator types
- Browser calls WebAuthn API:
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array([/* server-generated random bytes */]),
rp: {
name: “Example Corp”,
id: “example.com”
},
user: {
id: new Uint8Array([/* unique user identifier */]),
name: “[email protected]”,
displayName: “John Doe”
},
pubKeyCredParams: [
{ type: “public-key”, alg: -7 }, // ES256
{ type: “public-key”, alg: -257 } // RS256
],
authenticatorSelection: {
authenticatorAttachment: “platform”, // or “cross-platform”
userVerification: “required”,
residentKey: “required” // for passkeys
},
timeout: 60000,
attestation: “direct” // or “none”, “indirect”
}
});
- Authenticator generates keys: Creates a unique public/private key pair bound to the relying party’s origin
- Private key storage: Private key is stored in secure hardware (TPM, Secure Enclave, security key)
- Server receives: Public key, credential ID, and optional attestation for verification and storage
The Authentication Ceremony
Authentication occurs when a user logs in using their registered credential.
Step-by-Step Flow:
User → Clicks “Sign In”
Browser → Requests authentication options from server
Server → Returns challenge + allowed credentials
Browser → Calls navigator.credentials.get()
Authenticator → Prompts user to verify identity (biometric/PIN)
User → Approves authentication
Authenticator → Signs challenge with private key
Browser → Sends signed assertion to server
Server → Verifies signature with stored public key
Server → Returns authentication successful
Authentication API Call:
const assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array([/* server-generated random bytes */]),
rpId: “example.com”,
allowCredentials: [
{
type: “public-key”,
id: new Uint8Array([/* credential ID from registration */]),
transports: [“usb”, “nfc”, “ble”, “internal”]
}
],
userVerification: “required”,
timeout: 60000
}
});
Why This Is Secure: The Cryptographic Foundation
Public-Key Cryptography
- Each credential is a unique key pair (public + private)
- Private key never leaves the authenticator
- Server only stores the public key
- Even if server is breached, credentials cannot be used elsewhere
Origin Binding
- Credentials are cryptographically bound to the relying party’s origin
- A credential for bank.com cannot be used on evil-bank.com
- This binding is enforced by the browser, not user judgment
Challenge-Response
- Each authentication uses a fresh, random challenge
- Signed responses cannot be replayed
- No static secrets are transmitted
Phishing Resistance
- The combination of origin binding and challenge-response makes WebAuthn inherently phishing-resistant MFA
- Even if a user visits a phishing site, their credential cannot be used against the legitimate site
How Do I Add WebAuthn Passkeys to a dApp?
Integrating WebAuthn into decentralized applications (dApps) presents unique opportunities and challenges. This section provides practical guidance on how to add WebAuthn passkeys to a dApp.
Why WebAuthn for dApps?
Traditional dApps rely on wallet signatures for authentication, which has limitations:
- Users must have a wallet installed
- Private key management is complex
- Transaction signing UX is often confusing
- Mobile experience varies significantly
WebAuthn passkeys offer:
- Native browser support without extensions
- Familiar biometric authentication UX
- Hardware-backed key security
- Cross-platform compatibility
dApp Frontend Components:
- WebAuthn API Integration – Handles credential creation and authentication
- Web3 Library (ethers.js, wagmi) – Blockchain interactions
- UI Components – User interface elements
Backend Services:
- Backend Server
- Challenge generation
- Credential storage
- Session management
- Signature verification
- Blockchain
- Smart contracts
- Account abstraction
- Passkey wallet
Implementation Approaches
Approach 1: WebAuthn for dApp Authentication (Off-chain)
Use WebAuthn for authenticating users to your dApp backend, separate from blockchain transactions.
// Frontend: Registration
async function registerPasskey(username) {
// Get registration options from your backend
const optionsResponse = await fetch(‘/api/webauthn/register/options’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ username })
});
const options = await optionsResponse.json();
// Convert base64 strings to ArrayBuffers
options.challenge = base64ToArrayBuffer(options.challenge);
options.user.id = base64ToArrayBuffer(options.user.id);
// Create credential
const credential = await navigator.credentials.create({
publicKey: options
});
// Send credential to backend for verification and storage
const verifyResponse = await fetch(‘/api/webauthn/register/verify’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
response: {
attestationObject: arrayBufferToBase64(
credential.response.attestationObject
),
clientDataJSON: arrayBufferToBase64(
credential.response.clientDataJSON
)
},
type: credential.type
})
});
return await verifyResponse.json();
}
// Frontend: Authentication
async function authenticateWithPasskey() {
// Get authentication options
const optionsResponse = await fetch(‘/api/webauthn/authenticate/options’, {
method: ‘POST’
});
const options = await optionsResponse.json();
options.challenge = base64ToArrayBuffer(options.challenge);
options.allowCredentials = options.allowCredentials.map(cred => ({
…cred,
id: base64ToArrayBuffer(cred.id)
}));
// Get assertion
const assertion = await navigator.credentials.get({
publicKey: options
});
// Verify with backend
const verifyResponse = await fetch(‘/api/webauthn/authenticate/verify’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
id: assertion.id,
rawId: arrayBufferToBase64(assertion.rawId),
response: {
authenticatorData: arrayBufferToBase64(
assertion.response.authenticatorData
),
clientDataJSON: arrayBufferToBase64(
assertion.response.clientDataJSON
),
signature: arrayBufferToBase64(
assertion.response.signature
)
},
type: assertion.type
})
});
const result = await verifyResponse.json();
if (result.verified) {
// User authenticated – create session, etc.
}
}
Approach 2: Passkey-Based Smart Contract Wallets (On-chain)
Modern account abstraction (ERC-4337) enables passkeys to directly control smart contract wallets.
// Using a passkey-enabled smart wallet SDK (example structure)
import { PasskeyWallet } from ‘@example/passkey-wallet-sdk’;
async function createPasskeyWallet() {
// Create WebAuthn credential
const credential = await navigator.credentials.create({
publicKey: {
challenge: crypto.getRandomValues(new Uint8Array(32)),
rp: { name: “My dApp”, id: window.location.hostname },
user: {
id: crypto.getRandomValues(new Uint8Array(16)),
name: userEmail,
displayName: userName
},
pubKeyCredParams: [
{ type: “public-key”, alg: -7 } // ES256 (P-256)
],
authenticatorSelection: {
residentKey: “required”,
userVerification: “required”
}
}
});
// Extract public key from attestation
const publicKey = extractPublicKey(credential.response.attestationObject);
// Deploy smart contract wallet with passkey as signer
const wallet = await PasskeyWallet.create({
publicKey: publicKey,
credentialId: credential.id,
network: ‘mainnet’
});
return wallet;
}
async function signTransaction(wallet, transaction) {
// Get signature challenge from wallet
const challenge = await wallet.getSignatureChallenge(transaction);
// Sign with passkey
const assertion = await navigator.credentials.get({
publicKey: {
challenge: challenge,
rpId: window.location.hostname,
allowCredentials: [{
type: “public-key”,
id: base64ToArrayBuffer(wallet.credentialId)
}],
userVerification: “required”
}
});
// Submit signed transaction
const txHash = await wallet.submitTransaction(transaction, {
signature: assertion.response.signature,
authenticatorData: assertion.response.authenticatorData,
clientDataJSON: assertion.response.clientDataJSON
});
return txHash;
}
Backend Implementation (Node.js Example)
// Using @simplewebauthn/server library
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from ‘@simplewebauthn/server’;
const rpName = ‘My dApp’;
const rpID = ‘mydapp.com’;
const origin = `https://${rpID}`;
// Registration options endpoint
app.post(‘/api/webauthn/register/options’, async (req, res) => {
const { username } = req.body;
// Get user from database or create new
const user = await findOrCreateUser(username);
// Get existing credentials to exclude
const existingCredentials = await getUserCredentials(user.id);
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: username,
userDisplayName: user.displayName || username,
attestationType: ‘none’,
excludeCredentials: existingCredentials.map(cred => ({
id: cred.credentialID,
type: ‘public-key’,
transports: cred.transports
})),
authenticatorSelection: {
residentKey: ‘required’,
userVerification: ‘required’
}
});
// Store challenge for verification
await storeChallenge(user.id, options.challenge);
res.json(options);
});
// Registration verification endpoint
app.post(‘/api/webauthn/register/verify’, async (req, res) => {
const { body } = req;
const user = await getCurrentUser(req);
const expectedChallenge = await getStoredChallenge(user.id);
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID
});
if (verification.verified && verification.registrationInfo) {
const { credentialPublicKey, credentialID, counter } =
verification.registrationInfo;
// Store credential in database
await storeCredential({
credentialID,
credentialPublicKey,
counter,
userId: user.id,
transports: body.response.transports
});
}
res.json({ verified: verification.verified });
});
// Authentication verification endpoint
app.post(‘/api/webauthn/authenticate/verify’, async (req, res) => {
const { body } = req;
// Find credential in database
const credential = await findCredentialById(body.id);
const expectedChallenge = await getStoredChallenge(credential.userId);
const verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: {
credentialID: credential.credentialID,
credentialPublicKey: credential.credentialPublicKey,
counter: credential.counter
}
});
if (verification.verified) {
// Update counter to prevent replay attacks
await updateCredentialCounter(
credential.id,
verification.authenticationInfo.newCounter
);
// Create session
const session = await createUserSession(credential.userId);
res.cookie(‘session’, session.token, { httpOnly: true, secure: true });
}
res.json({ verified: verification.verified });
});
Key Considerations for dApp Integration
- Credential Storage
- Server-side credentials for off-chain auth
- On-chain public key storage for account abstraction
- Consider credential backup and recovery
- Cross-Chain Compatibility
- Passkey public keys work across any EVM chain
- Smart contract wallet addresses can be deterministic
- Consider multi-chain deployment strategy
- User Experience
- Provide clear onboarding flow
- Support multiple passkeys per account
- Implement graceful fallbacks
- Security Considerations
- Validate all responses server-side
- Implement proper challenge expiration
- Consider rate limiting
- Audit smart contracts thoroughly
Recommended Libraries
Library | Purpose | Platform |
@simplewebauthn/browser | Frontend WebAuthn | JavaScript |
@simplewebauthn/server | Backend verification | Node.js |
webauthn4j | Backend verification | Java |
py_webauthn | Backend verification | Python |
go-webauthn | Backend verification | Go |
passport-fido2 | Express middleware | Node.js |
WebAuthn Security Properties
Phishing Resistance
WebAuthn’s phishing resistance stems from origin binding:
- During registration, the credential is bound to the relying party ID (e.g., bank.com)
- During authentication, the browser automatically includes the actual origin in the signed data
- A phishing site at evil-bank.com cannot use credentials bound to bank.com
- The user doesn’t need to verify the URL – cryptography handles it
This makes WebAuthn a core component of phishing-resistant MFA strategies.
Replay Attack Prevention
Each authentication ceremony uses a unique challenge:
- Server generates random challenge (minimum 16 bytes)
- Authenticator signs the challenge with private key
- Signature is valid only for that specific challenge
- Captured signatures cannot be reused
Credential Isolation
WebAuthn credentials are isolated by design:
- Unique key pair generated per relying party
- No credential linkability between sites
- Compromising one site reveals nothing about other credentials
- Eliminates credential reuse vulnerabilities
Hardware-Backed Security
Private keys are protected by hardware:
- TPM (Trusted Platform Module) on Windows
- Secure Enclave on Apple devices
- TEE (Trusted Execution Environment) on Android
- Secure element on hardware security keys
Keys cannot be extracted even with full device access.
Browser and Platform Support
Current Support Status (2024)
Browser | Platform | WebAuthn Support | Passkey Sync |
Chrome | Windows | Full | Via Google Account |
Chrome | macOS | Full | Via Google Account |
Chrome | Android | Full | Via Google Account |
Safari | macOS | Full | Via iCloud Keychain |
Safari | iOS | Full | Via iCloud Keychain |
Firefox | Windows | Full | Limited |
Firefox | macOS | Full | Limited |
Edge | Windows | Full | Via Microsoft Account |
Samsung Internet | Android | Full | Via Samsung Pass |
Feature Detection
// Check if WebAuthn is available
function isWebAuthnAvailable() {
return window.PublicKeyCredential !== undefined;
}
// Check if platform authenticator is available
async function isPlatformAuthenticatorAvailable() {
if (!isWebAuthnAvailable()) return false;
return await PublicKeyCredential
.isUserVerifyingPlatformAuthenticatorAvailable();
}
// Check for conditional UI support (passkey autofill)
async function isConditionalUIAvailable() {
if (!isWebAuthnAvailable()) return false;
return await PublicKeyCredential
.isConditionalMediationAvailable?.() ?? false;
}
// Usage
async function checkSupport() {
const webauthn = isWebAuthnAvailable();
const platform = await isPlatformAuthenticatorAvailable();
const conditional = await isConditionalUIAvailable();
console.log(`WebAuthn: ${webauthn}`);
console.log(`Platform Authenticator: ${platform}`);
console.log(`Conditional UI (Passkey Autofill): ${conditional}`);
}
Passkeys: WebAuthn Evolution
What Are Passkeys?
Passkeys are the consumer-friendly evolution of WebAuthn credentials. The term was introduced by Apple, Google, and Microsoft to make passwordless authentication more accessible.
Key characteristics:
- Discoverable credentials (no username required to initiate)
- Synced across devices via platform ecosystems
- User-friendly terminology and UX
- Based on existing WebAuthn/FIDO2 standards
Synced vs. Device-Bound Passkeys
Feature | Synced Passkeys | Device-Bound Passkeys |
Storage | Cloud-synced | Single device only |
Recovery | Automatic via cloud | Requires backup method |
Security | High | Highest |
Portability | Across ecosystem devices | Physical device required |
Enterprise Control | Limited | Full |
Best For | Consumer apps | High-security enterprise |
Implementing Passkey Autofill (Conditional UI)
Modern browsers support passkey autofill, allowing users to select passkeys from the same UI as password autofill:
// Enable conditional mediation for passkey autofill
async function authenticateWithAutofill() {
const options = await fetchAuthenticationOptions();
try {
const assertion = await navigator.credentials.get({
publicKey: {
…options,
// Enable conditional UI
mediation: ‘conditional’
},
// Also required for conditional UI
mediation: ‘conditional’
});
return await verifyAssertion(assertion);
} catch (error) {
if (error.name === ‘NotAllowedError’) {
// User cancelled or no credential available
return null;
}
throw error;
}
}
HTML for conditional UI:
<input
type=”text”
id=”username”
autocomplete=”username webauthn”
placeholder=”Username or use passkey”
/>
Enterprise Deployment Considerations
Identity Provider Integration
Major identity providers support WebAuthn:
Microsoft Entra ID (Azure AD)
- Native passkey/security key support
- Windows Hello for Business integration
- Conditional Access policies for FIDO2
Okta
- WebAuthn authenticator enrollment
- Passwordless sign-in flows
- Admin controls for authenticator types
Google Workspace
- Security key enforcement
- Passkey support for Google accounts
- Advanced Protection Program
Deployment Best Practices
- Start with Security-Conscious Users
- IT and security teams
- Executives with access to sensitive data
- Users who have experienced phishing
- Require Multiple Authenticators
- Primary platform authenticator (laptop biometric)
- Backup security key
- Consider allowing synced passkeys for recovery
- Implement Progressive Enforcement
- Phase 1: Enable and encourage
- Phase 2: Require for sensitive operations
- Phase 3: Passwordless-only for eligible users
- Plan for Edge Cases
- Shared workstations
- Accessibility requirements
- Legacy system access
- Account recovery scenarios
Common Implementation Challenges
Challenge 1: Account Recovery
Problem: User loses all authenticators
Solutions:
- Require multiple authenticators during registration
- Implement secure recovery code generation
- Use identity verification for recovery (with appropriate security)
- Consider allowing synced passkeys as recovery option
Challenge 2: Cross-Device Authentication
Problem: User registers on laptop, needs to authenticate on phone
Solutions:
- Enable hybrid/cross-device authentication (QR + Bluetooth)
- Encourage platform authenticators that sync
- Support multiple credential types
Challenge 3: Legacy Browser Support
Problem: Some users have outdated browsers
Solutions:
- Implement graceful feature detection
- Provide fallback to traditional authentication
- Display upgrade recommendations
- Consider proxy-based solutions for legacy apps
Challenge 4: Enterprise Device Management
Problem: Managing authenticators across thousands of devices
Solutions:
- Integrate with MDM for platform authenticator management
- Use attestation to enforce approved authenticators
- Implement centralized credential lifecycle management
- Leverage identity provider policies
Future of WebAuthn
Emerging Capabilities
Signal API (Proposed)
- Allow relying parties to signal events to authenticators
- Use case: Mark compromised credentials for deletion
Device Public Key Extension
- Bind credentials to specific devices
- Enhanced security for high-risk scenarios
Hybrid Transport Improvements
- Better cross-device authentication UX
- Reduced latency for QR/Bluetooth flows
Enterprise Attestation
- More granular authenticator identification
- Better policy enforcement capabilities
Industry Adoption Trends
- Major platforms (Apple, Google, Microsoft) pushing passkeys aggressively
- Financial services adopting for customer authentication
- Healthcare organizations implementing for HIPAA compliance
- Government agencies mandating phishing-resistant MFA
Conclusion
WebAuthn represents the most significant advancement in authentication security since the introduction of MFA. By replacing shared secrets with asymmetric cryptography and binding credentials to specific origins, WebAuthn eliminates entire categories of attacks that have plagued password-based authentication for decades.
Key Takeaways:
- What is WebAuthn: A W3C standard enabling passwordless authentication using public-key cryptography
- How does WebAuthn work: Registration creates unique key pairs; authentication proves possession without revealing secrets
- How do I add WebAuthn passkeys to a dApp: Use the WebAuthn API with proper backend verification, or integrate with account abstraction for on-chain authentication
- Security benefits: Phishing resistance, replay protection, credential isolation, hardware-backed keys
- Passkeys: The consumer-friendly evolution of WebAuthn with cloud sync capabilities
Organizations implementing WebAuthn as part of their phishing-resistant MFA strategy gain protection against the credential-based attacks driving the majority of data breaches. Whether you’re securing a traditional web application or building the next generation of decentralized applications, WebAuthn provides the cryptographic foundation for truly secure authentication.
Ready to implement WebAuthn? TerraZone’s truePass platform provides enterprise-grade WebAuthn support with seamless integration, comprehensive management capabilities, and Zero Trust architecture. Contact us to learn how passwordless authentication can transform your security posture.


