Passwordless Authentication — Complete Implementation Guide
In this tutorial, you will learn about Passwordless Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
Passwordless authentication eliminates passwords by using alternative verification methods such as magic links sent via email, one-time codes delivered to trusted devices, or biometric authentication through WebAuthn, improving security and user experience simultaneously.
What You'll Learn
By the end of this lesson, you will implement magic link authentication, one-time passcode verification via email/SMS, configure WebAuthn for biometric authentication, and understand when passwordless is appropriate.
Why It Matters
Passwords are the weakest link in authentication — users reuse them, write them down, and fall for phishing. Passwordless auth eliminates these problems. Doda Browser uses magic link authentication for its web extension, allowing users to sync bookmarks without creating yet another password.
Real-World Use
A user wants to access their project management tool. They enter their email address. The tool sends a magic link. The user clicks it and is logged in. No password to remember, no password to leak. The link expires after 15 minutes and can only be used once.
Magic Link Flow
sequenceDiagram
participant User
participant App
participant Email
User->>App: Enter email address
App->>App: Generate token (crypto random, 15 min expiry)
App->>Email: Send magic link (https://app.com/auth/magic?token=abc)
Email-->>User: Check email
User->>App: Click magic link
App->>App: Verify token (exists, not expired, not used)
App->>App: Issue session/tokens
App-->>User: Authenticated
Magic Link Implementation
import secrets
import hashlib
from datetime import datetime, timedelta
class MagicLinkAuth:
def __init__(self):
self.tokens = {}
def generate_link(self, email):
token = secrets.token_urlsafe(48)
token_hash = hashlib.sha256(token.encode()).hexdigest()
self.tokens[token_hash] = {
"email": email,
"created_at": datetime.utcnow(),
"expires_at": datetime.utcnow() + timedelta(minutes=15),
"used": False,
}
magic_link = f"https://app.example.com/auth/magic?token={token}"
print(f"[MagicLink] Generated for {email}")
print(f"[MagicLink] Token hash: {token_hash[:16]}...")
print(f"[MagicLink] Send this link via email:")
print(f" {magic_link}")
return magic_link
def verify_link(self, token):
token_hash = hashlib.sha256(token.encode()).hexdigest()
stored = self.tokens.get(token_hash)
if not stored:
print(f"[MagicLink] Invalid token rejected")
return None
if stored["used"]:
print(f"[MagicLink] Already used token rejected")
return None
if datetime.utcnow() > stored["expires_at"]:
print(f"[MagicLink] Expired token rejected")
return None
stored["used"] = True
print(f"[MagicLink] Token verified for {stored['email']}")
return {"email": stored["email"], "auth_method": "magic_link"}
def cleanup_expired(self):
now = datetime.utcnow()
expired = [h for h, t in self.tokens.items() if now > t["expires_at"]]
for h in expired:
del self.tokens[h]
if expired:
print(f"[MagicLink] Cleaned {len(expired)} expired tokens")
auth = MagicLinkAuth()
link = auth.generate_link("alice@example.com")
result = auth.verify_link(link.split("=")[1])
print(f"Authenticated: {result['email'] if result else 'FAILED'}")
Expected output:
[MagicLink] Generated for alice@example.com
[MagicLink] Send this link via email:
https://app.example.com/auth/magic?token=abc123...
[MagicLink] Token verified for alice@example.com
Authenticated: alice@example.com
One-Time Code (OTP) via Email
const crypto = require("crypto");
const express = require("express");
const app = express();
app.use(express.json());
const otpStore = new Map();
function generateOTP(length = 6) {
return Array.from({ length }, () =>
crypto.randomInt(0, 10).toString()
).join("");
}
app.post("/api/auth/send-code", (req, res) => {
const { email } = req.body;
const code = generateOTP();
const hash = crypto.createHash("sha256").update(code).digest("hex");
otpStore.set(hash, {
email,
attempts: 0,
expiresAt: Date.now() + 600000,
});
console.log(`[OTP] Code for ${email}: ${code}`);
console.log(`[OTP] (In production, send via email/SMS)`);
res.json({ message: "Code sent", expiresIn: 600 });
});
app.post("/api/auth/verify-code", (req, res) => {
const { email, code } = req.body;
const hash = crypto.createHash("sha256").update(code).digest("hex");
const stored = otpStore.get(hash);
if (!stored || stored.email !== email) {
return res.status(401).json({ error: "Invalid code" });
}
if (Date.now() > stored.expiresAt) {
otpStore.delete(hash);
return res.status(401).json({ error: "Code expired" });
}
stored.attempts++;
if (stored.attempts > 3) {
otpStore.delete(hash);
return res.status(429).json({ error: "Too many attempts" });
}
otpStore.delete(hash);
console.log(`[OTP] Verified for ${email}`);
res.json({ session: "session-token", email });
});
app.listen(3000);
Common Mistakes
- Using predictable token generation (timestamps, sequential IDs) instead of cryptographic randomness.
- Not expiring magic links or OTP codes (should expire in 5-15 minutes).
- Allowing unlimited verification attempts on OTP codes (rate limit to 3-5 attempts).
- Sending magic links or codes via insecure channels (no plain text SMS for sensitive apps).
- Not invalidating tokens after use allows replay attacks with the same link.
- Confusing authentication (proving identity) with authorization (granting access) in passwordless flows.
Practice Questions
- Why are magic links more secure than passwords?
Magic links are cryptographically random, single-use, time-limited, and tied to a specific email address. They cannot be reused, guessed, or phished in the same way passwords can.
- How do you prevent brute force attacks on OTP codes?
Limit verification attempts to 3-5 per code, invalidate the code after failed attempts, use longer codes (8 digits), and implement exponential backoff between attempts.
- What is the difference between a magic link and an OTP?
Magic links are clicked and work as a one-time URL. OTP codes are manually entered. Magic links provide better UX (single click), while OTPs work on any device (phone, TV, CLI).
- Challenge: Build a complete passwordless auth system supporting email magic links, SMS OTP, and WebAuthn, with device trust (skip auth for 30 days on trusted devices) and backup email codes as fallback.
FAQ
Mini Project: Passwordless Auth CLI
Build a CLI tool that simulates a complete passwordless authentication flow: request magic link, simulate email delivery, verify the link, and issue a JWT.
import secrets
import hashlib
import jwt
import time
import sys
class PasswordlessCLI:
def __init__(self):
self.tokens = {}
self.secret = secrets.token_hex(32)
def request_login(self, email):
token = secrets.token_urlsafe(48)
h = hashlib.sha256(token.encode()).hexdigest()
self.tokens[h] = {"email": email, "exp": time.time() + 900}
print(f"\n[1] Login requested for {email}")
print(f"[2] Magic link generated (click in browser):")
print(f" https://app.com/auth/magic?token={token[:32]}...")
print(f"[3] Run: python auth.py verify {h[:16]}... <token>\n")
return token
def verify(self, token):
h = hashlib.sha256(token.encode()).hexdigest()
stored = self.tokens.pop(h, None)
if not stored:
return None
if time.time() > stored["exp"]:
return None
access = jwt.encode({
"sub": stored["email"], "method": "magic_link",
"exp": int(time.time()) + 900,
}, self.secret, algorithm="HS256")
return {"access_token": access, "email": stored["email"]}
def run_interactive(self):
email = input("Email: ").strip()
token = self.request_login(email)
input("\nPress Enter to simulate clicking the magic link...\n")
result = self.verify(token)
if result:
print(f"Authenticated as {result['email']}")
print(f"Token: {result['access_token'][:40]}...")
else:
print("Authentication failed")
if __name__ == "__main__":
cli = PasswordlessCLI()
cli.run_interactive()
What's Next
Learn about social login for third-party identity integration, then explore LDAP authentication for enterprise directory services.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro