Passwordless Authentication Implementation Guide
Passwords are the oldest and weakest link in authentication. They are phishable, reusable, guessable, and expensive to manage, because every forgotten password becomes a reset request. 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, Microsoft Entra ID, 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, SoloKeys):
- 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)
The example below uses the current @simplewebauthn/server API (v13 and later). Since v11 the library takes a credential object rather than the old authenticator argument, and userID must be a Uint8Array, not a string.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoUint8Array } from '@simplewebauthn/server/helpers';
const rpName = 'Your Company';
const rpID = 'yourcompany.com';
const origin = `https://${rpID}`;
app.post('/api/webauthn/register/begin', async (req, res) => {
const user = await getAuthenticatedUser(req);
const userPasskeys = await getUserPasskeys(user.id);
const options = await generateRegistrationOptions({
rpName,
rpID,
userName: user.email,
userDisplayName: user.name,
userID: isoUint8Array.fromUTF8String(user.webauthnUserID), // opaque handle, not the email
attestationType: 'none',
excludeCredentials: userPasskeys.map((passkey) => ({
id: passkey.id,
transports: passkey.transports,
})),
authenticatorSelection: {
residentKey: 'required',
userVerification: 'required',
},
});
await storeRegistrationOptions(user.id, options); // short TTL, single use
res.json(options);
});
app.post('/api/webauthn/register/complete', async (req, res) => {
const user = await getAuthenticatedUser(req);
const currentOptions = await getRegistrationOptions(user.id);
let verification;
try {
verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge: currentOptions.challenge,
expectedOrigin: origin,
expectedRPID: rpID,
requireUserVerification: true,
});
} catch (error) {
return res.status(400).json({ error: error.message });
}
if (verification.verified) {
const { credential, credentialDeviceType, credentialBackedUp } =
verification.registrationInfo;
await savePasskey({
userId: user.id,
webAuthnUserID: currentOptions.user.id,
id: credential.id,
publicKey: credential.publicKey, // Uint8Array: store as bytes
counter: credential.counter,
transports: credential.transports,
deviceType: credentialDeviceType, // 'singleDevice' or 'multiDevice'
backedUp: credentialBackedUp, // true for synced passkeys
});
}
res.json({ verified: verification.verified });
});
Use a well-tested library like @simplewebauthn/server (Node.js), py_webauthn (Python), or webauthn-rs (Rust) rather than implementing the cryptographic verification yourself. Storing deviceType and backedUp lets you tell synced passkeys from device-bound ones later, for example to require device-bound credentials for admins.
Step 4: Implement Authentication (Server Side)
import {
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
app.post('/api/webauthn/login/begin', async (req, res) => {
// Omit allowCredentials for the discoverable (username-less) passkey flow
const options = await generateAuthenticationOptions({
rpID,
userVerification: 'required',
});
await storeChallengeForSession(req.sessionId, options.challenge);
res.json(options);
});
app.post('/api/webauthn/login/complete', async (req, res) => {
const expectedChallenge = await getChallengeForSession(req.sessionId);
const passkey = await getPasskeyById(req.body.id);
if (!passkey) {
return res.status(401).json({ error: 'Unknown credential' });
}
let verification;
try {
verification = await verifyAuthenticationResponse({
response: req.body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
requireUserVerification: true,
credential: {
id: passkey.id,
publicKey: passkey.publicKey,
counter: passkey.counter,
transports: passkey.transports,
},
});
} catch (error) {
return res.status(400).json({ error: error.message });
}
if (verification.verified) {
const { newCounter } = verification.authenticationInfo;
await updatePasskeyCounter(passkey.id, newCounter);
await createSession(passkey.userId, req);
}
res.json({ verified: verification.verified });
});
Signature counters and synced passkeys. Do not treat a counter of 0 as an error. The WebAuthn spec says authenticators that do not implement a signature counter leave signCount "constant at zero", and synced passkeys commonly report 0 on every sign-in. The SimpleWebAuthn docs note that some authenticators, such as Touch ID on macOS, always return zero. Counter checks are only a cloning signal for credentials that actually increment, which in practice means device-bound authenticators.
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: For an authenticator that increments its counter, simulate a counter that goes backward (indicating a cloned authenticator) and verify that your server rejects the authentication. Confirm that synced passkeys reporting
0still sign in. - 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. Authenticators that implement a signature counter increment it with each use. If a non-zero counter goes backward, it may indicate a cloned authenticator: alert and investigate. Synced passkeys usually report
0, so the check gives no signal for them.
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 as attack vectors.
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.
Frequently asked questions
- What is the difference between FIDO2, WebAuthn, and passkeys?
- FIDO2 is the overall standard for public-key authentication. WebAuthn is its browser JavaScript API, through which a relying party calls navigator.credentials.create() to register and navigator.credentials.get() to authenticate. A passkey is a discoverable WebAuthn credential, often synced across a user's devices through iCloud Keychain or Google Password Manager, which solves the device-loss recovery problem of earlier FIDO2 deployments.
- Should passwordless use platform authenticators, security keys, or synced passkeys?
- Most organizations support all three. Make synced passkeys the default for most users, because they survive device loss, and require hardware security keys for high-privilege accounts such as admins and executives. Platform authenticators like Touch ID and Windows Hello sit in between, and roaming keys suit shared workstations where a platform authenticator is not available.
- How long does it take to go fully passwordless?
- Plan for about a year. Offer passkeys as an option alongside passwords in months one to three, make passkeys the default sign-in in months four to six, and in months seven to twelve disable password login for groups with more than 90 percent adoption while admins move to hardware keys. Delete stored passwords only after that, from month twelve on.
- Why is passwordless authentication phishing-resistant?
- Because the browser binds each WebAuthn credential to the origin it was registered for. A phishing site on a different domain cannot trigger the authenticator to sign for your relying party, so there is no code or secret for the user to hand over. That is the main advantage over passwords and one-time passcodes, which a real-time proxy can relay.
- Should we write our own WebAuthn verification code, and do we need an IdP?
- Do not write the cryptographic checks yourself. Use a tested library such as @simplewebauthn/server for Node.js, py_webauthn for Python, or webauthn-rs for Rust, which also lets you run WebAuthn directly in an app without an IdP. Your code still owns the challenge, the RP ID and user verification. An IdP adds central credential management across many apps.
- What happens if a user loses their phone?
- If the user registered a synced passkey, the credential is still available on any other device signed into the same Apple or Google account, so they sign in there and remove the lost device. If the credential was device-bound, they need a backup authenticator such as a security key, or they go through your account recovery process, which must end with registering a new credential.
- Do passkeys work on shared computers?
- Platform passkeys are a poor fit for shared computers because they are tied to the operating system user profile, so one person's credential can end up on a machine others use. For shared workstations, issue roaming authenticators such as hardware security keys, or use the cross-device flow, where the shared computer shows a QR code that the user scans with their own phone.
- What about users who cannot use biometrics?
- WebAuthn user verification does not require biometrics. Platform and roaming authenticators accept a device PIN as the alternative, so users with accessibility needs, or users on devices without biometric hardware, can enter a PIN instead of scanning a fingerprint or face. Hardware security keys support PIN-based user verification too, so test the PIN path as part of accessibility testing.
Related on Start with Identity
- GlossaryBiometric Authentication
Biometric authentication confirms a person by comparing a live measurement of a physical characteristic, such as a fingerprint or face, against a template captu
- GuideIdentity Federation Implementation Guide: Protocols, Trust, and Cross-Domain SSO
A step-by-step guide to implementing identity federation covering SAML, OIDC, and WS-Federation protocols, trust relationship configuration, attribute mapping,
- GuideMFA Implementation Best Practices
A complete guide to implementing multi-factor authentication, comparing MFA methods, deploying phishing-resistant MFA, balancing security with user experience,
- GuidePasswordless: a strategy that survives reality
Not all passwordless is equal. A practical strategy for choosing between magic links, OTP, and passkeys by threat model, rolling them out in the right order, an
- BlogRSA brings passwordless authentication to Linux servers, closing its last password-only gap
RSA ID Plus now covers Linux servers, developer workstations, and critical infrastructure with FIDO-based passwordless sign-in, closing the gap where organizati
- ArticleTop 8 Passwordless Authentication Platforms
Compare the leading passwordless authentication platforms in 2026, from HYPR and MojoAuth to Hanko and Descope, with features, architecture, and pricing.