"No password" sounds like less security, but it's often more — there's no reusable secret to phish, leak, or reuse across sites. Magic links and one-time codes both rest on a single move you've already seen twice: a short-lived, single-use secret sent to something you already control (your inbox). Today you'll build both and watch every attack against them bounce off.
🎯 Mission: a mental model of modern auth + the ability to wire it into a real appPasswordless replaces "something you remember" with "something you can receive." Instead of checking a stored secret, the app sends a fresh, expiring, single-use token to a channel you own (email) and logs you in if you can produce it. The token is the whole credential — so it must be high-entropy, time-limited, single-use, and stored only as a hash (Lesson 4) — exactly like the OAuth code (Lesson 5) was.
Lesson 4 showed how much can go wrong with passwords even when you do everything right: users reuse them across sites (one breach cascades), they get phished, and you carry the liability of a hashed-password store forever. Passwordless sidesteps the root cause — there's no long-lived shared secret to steal.
Magic link and OTP are the same protocol with a different delivery format — a clickable URL vs a typed code:
Notice the recurring spine: the method ends at "start a session." Passwordless is a phase-① login that, like all the others, hands off to phase ③.
The token is the whole credential, so it has to be airtight. Each property reuses something you already learned:
≥128 bits of randomness for a link (e.g. randomBytes(32)) — unguessable. (OTP gets away with 6 digits only because of the next two.)
Expire in ~15 minutes. Shrinks the window if the email leaks or sits in an inbox. (Like the OAuth code, Lesson 5.)
Burn it the instant it's redeemed. A used link/code is dead — so interception-after-use is worthless.
Save sha256(token), never the token. A DB leak then can't be used to log in. (Exactly Lesson 4's reasoning.)
A zero-dependency lab issues a magic link and an OTP, logs in with each, then throws every attack at them — reuse, forgery, expiry, brute force.
# in your terminal (no npm install needed):
cd ~/projects/learn/public/courses/auth/practice/passwordless-lab
node demo.js
magic link — every attack bounces: click the SAME link again → 🛡️ "token already used (single-use)" forged/guessed token → 🛡️ "invalid token" clicked 16 min later → 🛡️ "token expired" OTP brute force — 6 digits, but: guess #5 → 🛡️ wrong code guess #6 → 🛡️ too many attempts — lockedYou just built both passwordless methods from primitives you already knew (hash, expiry, single-use, rate-limit) and watched the four properties block four different attacks. Open
demo.js: the magic-link and OTP code paths are nearly identical — proof they're one idea in two costumes.| Magic link | OTP (emailed code) | |
|---|---|---|
| User action | Click a link | Type a 6-digit code |
| Cross-device | Awkward — link opens on the email's device | Easy — read on phone, type on laptop |
| Entropy | Huge (the token is the secret) | Tiny — leans on rate-limit + expiry |
| Good as a 2nd factor? | Not really | Yes — a true step-up factor |
| Main risk | Link forwarded / email security; link-scanners pre-fetching it | Phishable (user can be tricked into reading it aloud) |
Passwordless isn't magic. Two real caveats to say out loud:
Q1. A 6-digit OTP has only a million possibilities. Why isn't it trivially brute-forced?
Two guards make the small space safe: rate limiting (lock out after ~5 wrong tries — you watched guess #6 get locked) and short expiry (the code dies in ~15 min). An attacker gets a handful of guesses against a million-space target before lockout, then the code is gone. Rate-limit + expiry beat raw length. (§03 warning, §04.)
Q2. Why store only a hash of the magic-link token, when the token is already random and short-lived?
Same reason as passwords (Lesson 4): if your database leaks, plaintext tokens would let the attacker log in as any pending user during the validity window. Storing sha256(token) means a DB dump is useless for login — the attacker can't reverse the hash to the raw token the inbox holds. Defense for the "assume breach" case. (§03.)
Q3. Your magic links sometimes "expire before the user clicks them," and login emails show as already-read by a security appliance. What's happening and one fix?
A corporate email scanner / link-preview bot is pre-fetching the link, consuming the single-use token (or even completing login) before the human clicks. Fix: don't log in on the bare GET — land on a page with a "Confirm sign-in" button that POSTs, and/or bind the link to the originating session/device. (§05 warning.)
Passwordless removed the stored secret — but magic links and OTP still aren't truly phishing-proof, because a human can be tricked into relaying the token to a fake site. Lesson 9 is the endgame that closes that last hole: passkeys (WebAuthn / FIDO2). They use public-key cryptography bound to the real website's origin, so there's nothing to type, nothing to relay, and a phishing site simply can't get a usable signature. You'll generate a real key pair and watch a phishing attempt fail by construction.
💬 Ask me anything — e.g. "wire a magic link with real email (Nodemailer) in Express", "add the POST-confirm landing page", or "combine OTP as a second factor on top of passwords?" · Back to Lesson 7 · Lesson 4 · Resources.