Passwordless Authentication Implementation Guide
A hands-on guide to implementing passwordless authentication using FIDO2, WebAuthn, and passkeys, including platform authenticator setup, migration strategies, and enterprise rollout planning.
Passwords are the oldest and weakest link in authentication. They are phishable, reusable, guessable, and expensive to manage. The average enterprise spends 30-50% of its help-desk budget on password resets. Credential stuffing attacks exploit the fact that users reuse passwords across sites. Even sophisticated password policies cannot eliminate the fundamental problem: secrets that humans must remember and type are inherently vulnerable.
Passwordless authentication eliminates passwords entirely, replacing them with cryptographic credentials that are bound to the user's device, cannot be phished, and require no memorization. With the maturation of FIDO2, WebAuthn, and passkeys, passwordless is no longer experimental. It is production-ready and rapidly becoming the standard.
This guide walks you through implementing passwordless authentication, from understanding the underlying protocols to deploying passkeys across your organization.
What You Will Learn
- How FIDO2, WebAuthn, and passkeys work together
- Choosing between platform authenticators, roaming authenticators, and synced passkeys
- Server-side WebAuthn implementation
- Migration strategy from passwords to passwordless
- Enterprise rollout and user adoption techniques
Prerequisites
- HTTPS everywhere — WebAuthn only works over secure origins. Every application and service must be served over HTTPS.
- Modern browser support — Chrome, Safari, Firefox, and Edge all support WebAuthn. Verify that your user base is on supported versions.
- Identity Provider support — Your IdP must support FIDO2/WebAuthn. Okta, Azure AD, Ping Identity, and most modern IdPs do.
- User device inventory — Know what devices your users have. Platform authenticators require biometric hardware (Touch ID, Windows Hello, Android biometrics).
- Fallback mechanism — You need a fallback authentication method during the transition period. This could be OTP, magic links, or password + MFA.
Architecture Overview
Passwordless authentication using FIDO2/WebAuthn involves three parties:
- Relying Party (RP): Your application or IdP server. It generates challenges, stores public keys, and verifies assertions.
- Authenticator: The device or software that creates and stores the private key and performs the cryptographic operation. This can be a platform authenticator (built into the device, like Touch ID) or a roaming authenticator (external, like a YubiKey).
- Client (Browser): Mediates between the RP and the authenticator using the WebAuthn JavaScript API.
Registration flow:
- RP generates a random challenge and sends it to the browser.
- Browser calls
navigator.credentials.create()with the challenge and RP information. - Authenticator generates a new key pair, stores the private key, and returns the public key and a signed attestation.
- Browser sends the public key and attestation to the RP.
- RP validates the attestation and stores the public key associated with the user.
Authentication flow:
- RP generates a random challenge and sends it to the browser along with the user's credential IDs.
- Browser calls
navigator.credentials.get(). - Authenticator finds the matching credential, signs the challenge with the private key, and returns the assertion.
- Browser sends the signed assertion to the RP.
- RP verifies the signature using the stored public key. If valid, the user is authenticated.
Passkeys are a user-friendly evolution of FIDO2 credentials. They are WebAuthn credentials that are synced across the user's devices via the platform's cloud (iCloud Keychain for Apple, Google Password Manager for Android/Chrome). This solves the key recovery problem that plagued earlier FIDO2 deployments.
Step-by-Step Implementation
Step 1: Choose Your Authenticator Strategy
You have three options, and most organizations will support all three:
Platform Authenticators (Touch ID, Windows Hello, Android biometric):
- Built into the device, no additional hardware required
- Excellent user experience — users just scan a fingerprint or glance at the camera
- Credential is bound to the device (unless using synced passkeys)
Roaming Authenticators (YubiKey, Feitian, SoloKey):
- External USB/NFC/BLE devices
- Work across any device with a USB port or NFC reader
- Ideal for shared workstations or environments where platform authenticators are not available
Synced Passkeys (iCloud Keychain, Google Password Manager):
- WebAuthn credentials synced across the user's devices
- Solve the device-loss recovery problem
- Slight reduction in security assurance since the credential is no longer device-bound
For enterprise environments, the recommended approach is: synced passkeys as the default for most users, hardware security keys for high-privilege accounts (admins, executives), and platform authenticators as a middle ground.
Step 2: Configure the Relying Party
The RP configuration defines how your server interacts with WebAuthn. Key settings:
// Relying Party configuration
const rpConfig = {
rp: {
name: "Your Company",
id: "yourcompany.com" // Must match the origin's registrable domain
},
user: {
id: userIdBuffer, // Opaque user handle (NOT email)
name: "user@yourcompany.com",
displayName: "Jane Smith"
},
challenge: generateSecureRandom(32), // 32-byte random challenge
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256 (preferred)
{ type: "public-key", alg: -257 } // RS256 (fallback)
],
authenticatorSelection: {
authenticatorAttachment: "platform", // or "cross-platform" for security keys
residentKey: "required", // for passkeys/discoverable credentials
userVerification: "required" // biometric or PIN required
},
timeout: 60000, // 60 seconds
attestation: "none" // "direct" if you need attestation verification
};
Important decisions:
- Set
residentKey: "required"for passkeys (discoverable credentials). This enables username-less login. - Set
userVerification: "required"to ensure the authenticator verifies the user (biometric or PIN) before signing. - Use
attestation: "none"unless you have a regulatory requirement to verify the authenticator model. Attestation adds complexity.
Step 3: Implement Registration (Server Side)
// Registration endpoint
app.post('/api/webauthn/register/begin', async (req, res) => {
const user = await getAuthenticatedUser(req);
const challenge = generateSecureRandom(32);
await storeChallenge(user.id, challenge); // Store in session or cache with TTL
const options = {
rp: { name: "Your Company", id: "yourcompany.com" },
user: {
id: Buffer.from(user.id),
name: user.email,
displayName: user.name
},
challenge: challenge,
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 }
],
authenticatorSelection: {
residentKey: "required",
userVerification: "required"
},
timeout: 60000,
attestation: "none",
excludeCredentials: await getUserCredentials(user.id) // Prevent duplicate registration
};
res.json(options);
});
app.post('/api/webauthn/register/complete', async (req, res) => {
const user = await getAuthenticatedUser(req);
const { credential } = req.body;
const expectedChallenge = await getStoredChallenge(user.id);
// Verify the registration response
const verification = await verifyRegistration({
response: credential,
expectedChallenge,
expectedOrigin: "https://yourcompany.com",
expectedRPID: "yourcompany.com"
});
if (verification.verified) {
// Store the credential
await storeCredential({
userId: user.id,
credentialId: verification.registrationInfo.credentialID,
publicKey: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter,
transports: credential.response.getTransports?.() || []
});
res.json({ success: true });
}
});
Use a well-tested library like @simplewebauthn/server (Node.js), py_webauthn (Python), or webauthn-rs (Rust) rather than implementing the cryptographic verification yourself.
Step 4: Implement Authentication (Server Side)
// Authentication endpoint
app.post('/api/webauthn/login/begin', async (req, res) => {
const challenge = generateSecureRandom(32);
// For discoverable credentials (passkeys), no need to specify allowCredentials
const options = {
challenge: challenge,
rpId: "yourcompany.com",
userVerification: "required",
timeout: 60000
// allowCredentials omitted for passkey/discoverable credential flow
};
await storeChallengeForSession(req.sessionId, challenge);
res.json(options);
});
app.post('/api/webauthn/login/complete', async (req, res) => {
const { credential } = req.body;
const expectedChallenge = await getChallengeForSession(req.sessionId);
// Look up the credential by ID
const storedCredential = await getCredentialById(credential.id);
if (!storedCredential) {
return res.status(401).json({ error: "Unknown credential" });
}
const verification = await verifyAuthentication({
response: credential,
expectedChallenge,
expectedOrigin: "https://yourcompany.com",
expectedRPID: "yourcompany.com",
authenticator: {
credentialPublicKey: storedCredential.publicKey,
counter: storedCredential.counter
}
});
if (verification.verified) {
// Update the counter to detect cloned authenticators
await updateCredentialCounter(
storedCredential.id,
verification.authenticationInfo.newCounter
);
// Create session
await createSession(storedCredential.userId, req);
res.json({ success: true });
}
});
Step 5: Implement the Client Side
// Registration (client)
async function registerPasskey() {
const optionsRes = await fetch('/api/webauthn/register/begin', { method: 'POST' });
const options = await optionsRes.json();
// Convert base64 fields to ArrayBuffer
options.challenge = base64ToBuffer(options.challenge);
options.user.id = base64ToBuffer(options.user.id);
const credential = await navigator.credentials.create({ publicKey: options });
await fetch('/api/webauthn/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serializeCredential(credential))
});
}
// Authentication (client)
async function loginWithPasskey() {
const optionsRes = await fetch('/api/webauthn/login/begin', { method: 'POST' });
const options = await optionsRes.json();
options.challenge = base64ToBuffer(options.challenge);
const assertion = await navigator.credentials.get({ publicKey: options });
const result = await fetch('/api/webauthn/login/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serializeAssertion(assertion))
});
if (result.ok) {
window.location.href = '/dashboard';
}
}
Step 6: Plan the Migration
Going passwordless is not a switch you flip overnight. Use a phased approach:
Phase 1 — Passwordless as an option (Months 1-3): Users can register a passkey alongside their existing password. Promote passkey registration through in-app prompts and communications.
Phase 2 — Passwordless preferred (Months 4-6): Default the login page to passkey authentication. Users can still click "Sign in with password" as a fallback. Track adoption metrics.
Phase 3 — Passwordless enforced (Months 7-12): For groups with high adoption (>90%), disable password login. High-privilege users (admins) must use hardware security keys. Maintain a break-glass password reset process.
Phase 4 — Password elimination (Month 12+): Remove password fields from the database. Passwords no longer exist in your system.
Configuration Best Practices
- Support multiple credentials per user. Users should register at least two authenticators (e.g., platform authenticator + security key) so they have a backup if one is lost.
- Use discoverable credentials. Set
residentKey: "required"to enable the passkey flow where users do not need to type a username. - Do not require attestation unless you have a specific compliance need. Attestation complicates registration and causes compatibility issues with some authenticators.
- Set reasonable timeouts. 60 seconds is reasonable for registration and authentication. Longer timeouts increase the risk of challenge replay.
- Store credentials securely. The public key is not secret, but the credential ID and user association must be protected. Use the same security controls as your user database.
- Implement credential management UI. Give users a self-service page to view registered credentials, rename them, and delete lost ones.
Testing and Validation
- Cross-browser testing: Test registration and authentication in Chrome, Safari, Firefox, and Edge on both desktop and mobile.
- Authenticator diversity: Test with platform authenticators (Touch ID, Windows Hello), hardware security keys (YubiKey 5), and synced passkeys (iCloud, Google).
- Error handling: Test what happens when the user cancels the biometric prompt, uses the wrong finger, or removes the security key mid-operation.
- Counter verification: Simulate a counter mismatch (indicating a cloned authenticator) and verify that your server rejects the authentication.
- Recovery flow: Simulate a user losing their device. Verify that the recovery process works and that the lost credential can be revoked.
- Accessibility testing: Ensure that users who cannot use biometrics can fall back to a PIN on the authenticator.
Common Pitfalls and Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| NotAllowedError during registration | RP ID does not match the origin | Ensure rpId is the registrable domain of your origin |
| User cannot find their passkey | Credential was not discoverable | Set residentKey: "required" during registration |
| Authentication works on laptop but not phone | Credential was created with authenticatorAttachment: "platform" on laptop | Use synced passkeys or register credentials on each device |
| "This passkey already exists" | User trying to re-register an existing credential | Use excludeCredentials in registration options |
| Safari blocks the WebAuthn prompt | User gesture requirement not met | Ensure the WebAuthn call is triggered by a direct user click, not a timer or page load |
Security Considerations
- Phishing resistance. WebAuthn is inherently phishing-resistant because the browser binds the credential to the origin. A phishing site on a different domain cannot trigger the authenticator. This is the single biggest security advantage over passwords and OTPs.
- Device loss. If a user loses their only registered authenticator, they are locked out. Require at least two registered credentials and provide a secure account recovery process.
- Synced passkey security model. Synced passkeys trade device-binding for usability. The private key is stored in a cloud vault (iCloud Keychain, Google Password Manager), which means the security of the credential depends on the security of the user's cloud account. For high-security environments, require device-bound credentials (hardware security keys).
- Replay protection. Always generate a fresh, random challenge for each authentication ceremony. Never reuse challenges.
- Counter tracking. The authenticator increments a counter with each use. If the counter goes backward, it may indicate a cloned authenticator. Alert and investigate.
Conclusion
Passwordless authentication is the most significant improvement in authentication security in decades. By eliminating passwords, you eliminate phishing, credential stuffing, password spraying, and password reuse — attack vectors that account for the majority of breaches.
The technology is mature, the standards are finalized, and the user experience is superior to passwords. The main challenge is organizational: migrating users, supporting diverse devices, and handling edge cases. By following the phased migration approach outlined in this guide and supporting multiple authenticator types, you can transition your organization to passwordless with minimal disruption.
FAQs
Q: What happens if a user loses their phone? A: If the user registered a synced passkey, their credential is available on any other device signed into the same cloud account. If they registered a device-bound credential, they need a backup authenticator (e.g., security key) or must go through account recovery.
Q: Are passkeys secure enough for enterprise use? A: Yes. Passkeys are phishing-resistant, cannot be reused across sites, and require user verification (biometric or PIN). For the highest security roles, supplement synced passkeys with device-bound hardware security keys.
Q: Can I use WebAuthn without an IdP? A: Yes. You can implement WebAuthn directly in your application using a server library. However, using an IdP simplifies credential management and provides a centralized authentication experience across multiple applications.
Q: Do passkeys work on shared computers? A: Platform passkeys on shared computers are problematic because they are tied to the OS user profile. Use roaming authenticators (security keys) or QR-code-based cross-device authentication for shared workstations.
Q: What about users who cannot use biometrics? A: WebAuthn authenticators support PIN as an alternative to biometrics. Users with accessibility needs can enter a PIN instead of scanning a fingerprint. Hardware security keys also support PIN-based user verification.
Share this article