Authentication Patterns in Distributed Systems
Passwordless Authentication
Read aloud is unavailable in this browser.
The End of the Password Era
Passwords are a 60-year-old technology born in an era when computer time was billed by the minute and security threats were confined to local terminals. Today, they represent the single largest attack surface in most organizations—responsible for over 80% of hacking-related breaches according to Verizon's Data Breach Investigations Report.
The industry is finally converging on a replacement. In 2022, Apple, Google, and Microsoft jointly announced support for passkeys—a FIDO2-based passwordless credential that synchronizes across devices. Major services including PayPal, eBay, Best Buy, and GitHub have deployed passkeys. The passwordless future isn't theoretical anymore; it's actively being built.
Passwordless authentication removes the shared secret (password) from the authentication flow entirely. Instead, users prove their identity through cryptographic keys, biometrics, possession of a device, or verification via a trusted channel. This eliminates entire classes of attacks: no password to phish, stuff, or brute-force.
This page covers the full landscape of passwordless authentication: magic links, one-time passwords, passkeys (discoverable WebAuthn credentials), biometric-only authentication, device-based flows, and strategic approaches for transitioning users from passwords to passwordless. You'll understand both the opportunities and challenges of eliminating passwords.
The Case Against Passwords
Before exploring passwordless solutions, it's worth understanding exactly why passwords have become so problematic. The issues aren't merely technical—they're fundamental to how passwords work.
Passwords Are Shared Secrets:
Every password-based system requires both the user and the server to know (or be able to derive) the same secret. This creates multiple points of failure:
- The password exists in the user's memory (recoverable via phishing, social engineering)
- The password traverses networks during login (interceptable if TLS misconfigured)
- The password (or its hash) is stored server-side (exposed in breaches)
Cryptographic authentication (like FIDO2) eliminates sharing: the server only knows the public key, which is useless to attackers.
| Password Problem | Root Cause | Passwordless Solution |
|---|---|---|
| Phishing | Users can be tricked into entering credentials on fake sites | Passkeys are cryptographically bound to legitimate domains |
| Credential Stuffing | Password reuse across breached sites | No password to reuse; unique keypair per service |
| Brute Force | Weak or predictable passwords | No password to guess; cryptographic proof |
| Rainbow Tables | Pre-computed hash lookups | No server-side password storage |
| Shoulder Surfing | Password observation during entry | Biometric or device-based; nothing to observe |
| Credential Database Breach | Hashed passwords stolen and cracked | Only public keys stored; useless to attackers |
| Password Fatigue | Too many passwords to remember | Passkeys sync across devices; no memorization |
| Social Engineering | Convincing users to reveal passwords | Users don't know a secret; nothing to reveal |
The fundamental shift in passwordless authentication is from shared secrets (symmetric) to public-key cryptography (asymmetric). With asymmetric keys, the private key never leaves the user's device, and the public key stored by the service cannot be used to impersonate the user. This architectural change eliminates the attack surface that passwords create.
Why Now?
Passwordless authentication isn't new—public-key authentication for SSH has existed for decades. What's changed:
-
Platform Support — iOS, Android, Windows, and macOS now have built-in passkey support with biometric authentication.
-
Credential Sync — Passkeys can sync across devices via cloud (iCloud Keychain, Google Password Manager), solving the 'what if I lose my device' problem.
-
WebAuthn Standard — A W3C standard implemented in all major browsers provides a consistent API for passwordless authentication.
-
User Familiarity — Users routinely unlock phones with fingerprints and faces; this mental model transfers to authentication.
-
Industry Coordination — FIDO Alliance brought Apple, Google, Microsoft, and major enterprises together around a common standard.
Magic Links: Email-Based Passwordless
Magic links are the simplest form of passwordless authentication: the user enters their email, receives a link with a cryptographic token, and clicking the link authenticates them. Services like Slack, Medium, and Notion popularized this approach.
How Magic Links Work:
- •Email Entry — User enters their email address on the login page.
- •Token Generation — Server generates a cryptographically random token (256-bit) and stores a hash with expiration time.
- •Email Dispatch — Server sends an email containing https://app.com/auth?token=xxxxx.
- •Token Validation — User clicks link; server validates token hash hasn't expired and hasn't been used.
- •Session Creation — Server creates authenticated session; token is invalidated (single use).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
import crypto from 'crypto'; interface MagicLinkToken { tokenHash: string; userId: string; expiresAt: Date; used: boolean; createdAt: Date; ipAddress: string; userAgent: string;} const TOKEN_EXPIRY_MINUTES = 15;const TOKEN_LENGTH_BYTES = 32; // 256 bits async function sendMagicLink(email: string, req: Request): Promise<void> { // Find or create user let user = await db.users.findByEmail(email); if (!user) { // Optionally create account on first login user = await db.users.create({ email, emailVerified: true }); } // Rate limiting: max 3 magic links per email per 15 minutes const recentTokens = await db.magicTokens.count({ userId: user.id, createdAt: { $gt: new Date(Date.now() - 15 * 60 * 1000) } }); if (recentTokens >= 3) { throw new Error('Too many login requests. Please check your email or wait.'); } // Generate token const token = crypto.randomBytes(TOKEN_LENGTH_BYTES).toString('base64url'); const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); // Store hashed token (not plaintext) await db.magicTokens.create({ tokenHash, userId: user.id, expiresAt: new Date(Date.now() + TOKEN_EXPIRY_MINUTES * 60 * 1000), used: false, createdAt: new Date(), ipAddress: req.ip, userAgent: req.headers['user-agent'], }); // Send email const magicUrl = `https://app.com/auth/magic?token=${token}`; await sendEmail(email, 'Your login link', ` Click to log in: ${magicUrl} This link expires in ${TOKEN_EXPIRY_MINUTES} minutes. If you didn't request this, ignore this email. `);} async function verifyMagicLink(token: string): Promise<User | null> { const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); const magicToken = await db.magicTokens.findOne({ tokenHash, used: false, expiresAt: { $gt: new Date() } }); if (!magicToken) { return null; // Invalid, expired, or already used } // Mark as used immediately (atomic operation) const updated = await db.magicTokens.updateOne( { tokenHash, used: false }, { $set: { used: true, usedAt: new Date() } } ); if (updated.modifiedCount === 0) { return null; // Race condition: already used } // Fetch user and create session const user = await db.users.findById(magicToken.userId); return user;}- •No password to remember or manage
- •Email is familiar and universal
- •No additional hardware or app required
- •Implicit email verification
- •Simple implementation
- •Email delivery delays frustrate users
- •Email is not encrypted in transit
- •Shared/corporate email risks forwarding
- •Device switching (receive on phone, open on desktop) can break flow
- •Account security tied to email account security
If an attacker controls a user's email account, magic links provide zero protection—they can request and use the link themselves. For high-security applications, magic links alone are insufficient. Consider combining with device recognition or treating magic link access as lower-privilege until additional verification occurs.
One-Time Passwords for Primary Authentication
One-time passwords (OTPs) are familiar from MFA: a 6-digit code delivered via SMS, email, or authenticator app. In passwordless contexts, OTPs serve as the primary authentication method rather than a second factor.
OTP vs Magic Link Trade-offs:
| Aspect | OTP | Magic Link |
|---|---|---|
| User Action | Enter 6-digit code manually | Click link |
| Cross-Device | Works easily (read on phone, type on laptop) | Problematic (link opens on receiving device) |
| Entry Errors | Typos possible | None (click-based) |
| Interception | Can be read from notification preview | URL in email body |
| Expiry | Typically 5-10 minutes | Typically 15-30 minutes |
| User Preference | Familiar from MFA | Magical/seamless feeling |
Implementation Considerations:
Rate Limiting: OTP entry is susceptible to brute force (10^6 possibilities for 6 digits). Implement strict rate limits:
- Maximum 5 failed attempts per code
- Code becomes invalid after 5 failures (generate new)
- Account lockout after 15 failures across multiple codes
- Exponential backoff between attempts
Delivery Channels:
- SMS — Widest reach but least secure (SIM swapping, SS7 vulnerabilities). Consider for low-risk apps only.
- Email — Better security than SMS; delays possible. Similar concerns to magic links.
- Authenticator App (TOTP) — More secure but requires setup. For passwordless, app generates codes without prior shared secret being established.
Hybrid Approach: Many services use 'email with code' rather than magic link—combining the benefits of both:
- Code can be entered on any device
- No URL to intercept
- User explicitly confirms by typing code
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
interface OTPRecord { codeHash: string; userId: string; expiresAt: Date; attempts: number; maxAttempts: number;} const OTP_LENGTH = 6;const OTP_EXPIRY_MINUTES = 10;const MAX_ATTEMPTS = 5; // Generate numeric OTPfunction generateOTP(): string { // Use crypto for randomness const bytes = crypto.randomBytes(4); const num = bytes.readUInt32BE() % 1000000; return num.toString().padStart(OTP_LENGTH, '0');} async function sendEmailOTP(email: string): Promise<void> { const user = await db.users.findByEmail(email); if (!user) { // Don't reveal whether user exists // Still send email with "no account" message await sendEmail(email, 'Login attempt', 'No account exists for this email. Click here to sign up.'); return; } // Invalidate any existing OTPs for this user await db.otpRecords.deleteMany({ userId: user.id }); const code = generateOTP(); const codeHash = crypto.createHash('sha256').update(code).digest('hex'); await db.otpRecords.create({ codeHash, userId: user.id, expiresAt: new Date(Date.now() + OTP_EXPIRY_MINUTES * 60 * 1000), attempts: 0, maxAttempts: MAX_ATTEMPTS, }); await sendEmail(email, 'Your login code', ` Your verification code is: ${code} This code expires in ${OTP_EXPIRY_MINUTES} minutes. If you didn't request this, ignore this email. `);} async function verifyEmailOTP( email: string, submittedCode: string): Promise<{ success: boolean; user?: User; error?: string }> { const user = await db.users.findByEmail(email); if (!user) { return { success: false, error: 'invalid_code' }; } const record = await db.otpRecords.findOne({ userId: user.id, expiresAt: { $gt: new Date() } }); if (!record) { return { success: false, error: 'code_expired' }; } if (record.attempts >= record.maxAttempts) { await db.otpRecords.deleteOne({ _id: record._id }); return { success: false, error: 'max_attempts_exceeded' }; } // Increment attempts before checking (prevents race conditions) await db.otpRecords.updateOne( { _id: record._id }, { $inc: { attempts: 1 } } ); const submittedHash = crypto.createHash('sha256') .update(submittedCode) .digest('hex'); if (submittedHash !== record.codeHash) { const remaining = record.maxAttempts - record.attempts - 1; return { success: false, error: 'invalid_code', message: `Invalid code. ${remaining} attempts remaining.` }; } // Success - delete OTP record await db.otpRecords.deleteOne({ _id: record._id }); return { success: true, user };}Passkeys: The Future of Authentication
Passkeys are the consumer-friendly name for FIDO2 discoverable credentials (also called resident keys). They represent the most significant advancement in authentication since the invention of passwords—providing phishing-resistant, cryptographic authentication that syncs across devices.
What Makes Passkeys Different:
- •Phishing-Resistant — Cryptographically bound to the legitimate domain; unusable on look-alike sites.
- •No Shared Secrets — Private key never leaves the device; server stores only public key.
- •Biometric-Protected — Unlocked via Face ID, Touch ID, Windows Hello; user verification built in.
- •Cross-Device Sync — Synced via iCloud Keychain, Google Password Manager, or password managers like 1Password.
- •Discoverable — The authenticator stores the user identifier, enabling usernameless login.
- •Passwordless-First — Designed as primary authentication, not just second factor.
Passkey User Experience:
-
Registration: User clicks 'Create Passkey'; browser prompts for biometric; device generates keypair and stores in secure enclave; public key sent to server.
-
Login: User clicks 'Sign in with Passkey'; browser shows available passkeys for this site; user selects and authenticates biometrically; device signs challenge with private key; server verifies signature.
The entire flow takes seconds and requires no typing.
Passkey Ecosystem:
Passkeys can be stored in:
- Platform authenticators — iCloud Keychain (Apple), Google Password Manager (Android/Chrome), Windows Hello
- Password managers — 1Password, Dashlane, Bitwarden (act as passkey providers)
- Hardware security keys — YubiKey, SoloKeys (non-synced, single device)
Platform passkeys sync automatically across devices signed into the same account. A passkey created on an iPhone is immediately available on iPad, Mac, and Safari on any device.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
// Registration Flowasync function registerPasskey(username: string) { // 1. Get registration options from server const optionsResponse = await fetch('/api/passkeys/register/options', { method: 'POST', body: JSON.stringify({ username }), }); const options = await optionsResponse.json(); // 2. Create credential using WebAuthn API const credential = await navigator.credentials.create({ publicKey: { challenge: base64urlToBuffer(options.challenge), rp: { name: 'My Application', id: 'myapp.com', }, user: { id: base64urlToBuffer(options.user.id), name: username, displayName: username, }, pubKeyCredParams: [ { alg: -7, type: 'public-key' }, // ES256 { alg: -257, type: 'public-key' }, // RS256 ], authenticatorSelection: { residentKey: 'required', // Make discoverable userVerification: 'required', // Require biometric/PIN }, timeout: 60000, }, }) as PublicKeyCredential; // 3. Send credential to server for verification const attestationResponse = credential.response as AuthenticatorAttestationResponse; await fetch('/api/passkeys/register/verify', { method: 'POST', body: JSON.stringify({ id: bufferToBase64url(credential.rawId), response: { clientDataJSON: bufferToBase64url(attestationResponse.clientDataJSON), attestationObject: bufferToBase64url(attestationResponse.attestationObject), }, }), });} // Login Flow (Usernameless)async function loginWithPasskey() { // 1. Get authentication options (no username needed!) const optionsResponse = await fetch('/api/passkeys/login/options', { method: 'POST', }); const options = await optionsResponse.json(); // 2. Get assertion using WebAuthn API const credential = await navigator.credentials.get({ publicKey: { challenge: base64urlToBuffer(options.challenge), rpId: 'myapp.com', timeout: 60000, userVerification: 'required', // Empty allowCredentials = discoverable credential flow // Browser shows all passkeys for this site }, }) as PublicKeyCredential; // 3. Send assertion to server const assertionResponse = credential.response as AuthenticatorAssertionResponse; const result = await fetch('/api/passkeys/login/verify', { method: 'POST', body: JSON.stringify({ id: bufferToBase64url(credential.rawId), response: { clientDataJSON: bufferToBase64url(assertionResponse.clientDataJSON), authenticatorData: bufferToBase64url(assertionResponse.authenticatorData), signature: bufferToBase64url(assertionResponse.signature), userHandle: assertionResponse.userHandle ? bufferToBase64url(assertionResponse.userHandle) : null, }, }), }); // User is now logged in!}As of 2024, passkeys have broad support: iOS 16+, Android 9+, macOS Ventura+, Windows 10/11, Chrome, Safari, Edge, and Firefox. Major sites including Google, PayPal, GitHub, and Adobe have deployed passkeys. The technology is mature enough for production use.
Device-Based Authentication
Device-based authentication uses a trusted device as the authentication factor. Once a device is enrolled, its mere possession (verified cryptographically) grants access. This approach is common in enterprise mobile device management (MDM) and increasingly in consumer apps.
Device Authentication Patterns:
| Pattern | How It Works | Use Cases |
|---|---|---|
| Device Certificate | Device holds a private key; certificate issued during enrollment | Enterprise MDM, VPN access, server-to-server mTLS |
| OAuth Device Flow | Device displays code; user authorizes on another device | Smart TVs, CLI tools, IoT devices |
| Trusted Device Token | Encrypted token stored on device after initial authentication | Mobile apps, 'Remember this device' for reduced MFA |
| Cross-Device Authentication | QR code on new device; scan with authenticated phone | Password manager browser extensions, new device setup |
| Proximity-Based | Authenticated device nearby via Bluetooth/NFC | Apple Watch unlock, Windows Dynamic Lock |
Cross-Device Authentication Flow:
This is particularly useful for authenticating on devices without capable input (shared computers, kiosks, new devices):
- •New Device — User wants to log in on a laptop without credentials available.
- •QR Code Display — Laptop shows QR code containing a short-lived session token.
- •Phone Scan — User scans QR with their authenticated mobile app.
- •Verification Prompt — Phone app shows 'Approve login on Chrome Windows in San Francisco?'
- •User Approves — User confirms via biometric on phone.
- •Session Transfer — Server creates authenticated session on laptop.
- •Login Complete — Laptop refreshes; user is logged in.
Device Trust Considerations:
Device-based authentication raises important security questions:
-
Device Compromise — If a device is stolen or infected with malware, what access does the attacker gain? Consider requiring biometric unlock before authentication actions.
-
Device Revocation — How do you immediately revoke trust when a device is lost? Maintain a device registry with revocation capability.
-
Shared Devices — Some devices (family iPad, work laptop) have multiple users. Device identity ≠ user identity in these cases.
-
Device Attestation — How do you verify the device hasn't been rooted/jailbroken? Mobile platforms offer attestation APIs (SafetyNet, App Attest).
The most secure pattern combines device identity with user verification: the device holds a cryptographic key that proves its identity, but using that key requires the legitimate user to authenticate locally (biometric, PIN). This is exactly how passkeys work and why they're effective.
Biometric-Only Authentication Patterns
Biometrics (fingerprint, face, voice) provide convenient user verification, but using them as the sole authentication factor requires careful design. Biometrics have properties that distinguish them from other factors.
Biometric Considerations:
- •Cannot Be Revoked — Unlike passwords, you can't change your fingerprint if compromised. Biometric templates must be protected accordingly.
- •Probabilistic Matching — Biometrics use similarity thresholds, not exact matches. There are always false accept rates (FAR) and false reject rates (FRR).
- •Privacy Sensitive — Biometric data is covered by regulations (BIPA, GDPR). Consent, storage, and processing requirements apply.
- •Spoofing Attacks — Photos, videos, fingerprint molds, and voice recordings can sometimes fool sensors. Liveness detection is essential.
- •Enrollment Quality — Authentication is only as good as the initial enrollment. Poor enrollment leads to false rejects.
Local vs Remote Biometric Verification:
Local verification (preferred): Biometric matching happens on-device. The server never sees biometric data. Only a cryptographic assertion of successful verification is sent. This is how Face ID and Touch ID work—Apple never receives your biometric template.
Remote verification: Biometric data (or template) is sent to a server for matching. This is problematic because:
- Biometric data in transit is vulnerable to interception
- Centralized biometric databases are high-value breach targets
- Regulatory compliance becomes complex
Best Practice: Use biometrics as a local unlock mechanism for a cryptographic credential (like passkeys). The biometric never leaves the device; it merely authorizes use of the private key.
| Biometric | Convenience | Security | Accessibility Concerns |
|---|---|---|---|
| Fingerprint | High (one touch) | High (modern sensors) | Users with worn fingerprints, certain disabilities |
| Face Recognition | Very High (passive) | High (3D sensors) / Medium (2D) | Masks, twins, low light, some disabilities |
| Iris Scan | Medium (requires positioning) | Very High | Specialized hardware, glasses interference |
| Voice Print | Medium (speaking phrase) | Medium (vulnerable to recording) | Speech disabilities, background noise |
| Behavioral (typing) | Very High (continuous) | Medium (supplemental) | Inconsistency, learning curve |
Never make biometrics the only way to authenticate. Users with disabilities, injuries, or environmental conditions may be unable to use a particular biometric. Always offer a fallback (PIN, passkey on hardware key, backup codes). Accessibility isn't just good ethics—it's often a legal requirement.
Transitioning to Passwordless
Moving from password-based to passwordless authentication is a multi-phase journey requiring careful planning, user education, and fallback strategies. You can't flip a switch and eliminate passwords overnight.
Transition Phases:
- •Phase 1: Enable Passwordless as Option — Add passkey/magic link support alongside passwords. Let early adopters try it. Measure adoption and issues.
- •Phase 2: Promote Passwordless — Suggest passwordless at registration and login. 'Try signing in with Face ID—it's faster!' Track conversion rates.
- •Phase 3: Passwordless as Default — New accounts default to passwordless. Existing users still have passwords. Registration flow emphasizes passkeys.
- •Phase 4: Deprecate Passwords — Prompt password users to add passkeys. 'Add a passkey to access your account faster and more securely.'
- •Phase 5: Sunset Passwords — Announce timeline for password removal. Provide migration path. Eventually disable password login.
Migration Challenges:
| Challenge | Description | Mitigation |
|---|---|---|
| Legacy Device Support | Older devices may not support passkeys | Maintain email OTP/magic link as fallback |
| User Hesitation | Users unfamiliar with new authentication | Clear in-app education, help articles, videos |
| Lost Device Recovery | No passkey if device lost/replaced | Multiple passkeys, backup codes, recovery flow |
| Enterprise Requirements | Corporate IT may have password policies | Support both; let admins choose enforcement |
| B2B Integration | Some APIs use password/API key auth | OAuth 2.0 for machine access; deprecate password-based API auth |
| Password Manager Habits | Users rely on password managers | Major PMs now support passkeys; communicate this |
Measuring Success:
Track metrics throughout the transition:
- Adoption Rate — Percentage of users with passkeys enrolled
- Login Method Distribution — What % of logins use passkeys vs. password vs. magic link
- Authentication Success Rate — Do passwordless methods have lower failure rates?
- Support Tickets — Are users struggling with the new flow?
- Time to Authenticate — Is passwordless actually faster?
- Security Incidents — Reduction in account takeovers, password reset abuse
Communication Strategy:
Users must understand what's changing and why:
- Frame as an upgrade: 'Log in faster with your fingerprint'
- Emphasize security: 'Your account is now protected against phishing'
- Be transparent: 'We're moving away from passwords because [reasons]'
- Provide timelines: 'Password login will be disabled on [date]'
- Offer help: 'Having trouble? Here's how to set up passkeys...'
Begin passwordless rollout with users most motivated by security (enterprise admins, financial account holders) or those seeking convenience (power users who log in frequently). Their positive experiences will help drive broader adoption.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.