Design a secure authentication system from scratch — walk through registration, login, session/token issuance, and password reset
The highest-yield senior interview question here — a strong answer walks every stage in order, naming the exact decision and failure mode at each one.
LEARN BY DOINGPut your intuition to work3 steps
Design a secure authentication system from scratch — walk through registration, login, session/token issuance, and password reset
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why this question is really a systems-design question wearing a security hat
"Design an authentication system" is one of the highest-yield senior interview questions precisely because it forces you to make a dozen small decisions, each of which has a well-known wrong answer that keeps showing up in real breaches: rolling your own password hashing, returning different errors for "wrong password" versus "no such account," storing a password-reset token in plaintext, never rotating refresh tokens, putting a long-lived token somewhere JavaScript can read it. A strong answer walks through registration, login, token issuance, and password reset in order, naming the specific decision at each step and why the alternative is a known failure mode — not reciting a list of buzzwords.
#Registration
Registration has to accomplish three things at once: create the account, prove the person controls the email address they claim to, and never leak whether an email address is already in the system to someone who isn't authorized to know that.
- Validate input server-side. Client-side password-strength meters are a UX nicety, not a control — the server must independently enforce the real policy, because nothing stops a request from skipping the browser entirely.
- Follow modern password-policy guidance, not composition rules. NIST 800-63B's guidance (which most serious systems now follow) favors a reasonable minimum length over forced complexity — length matters far more to resistance against cracking than mandating a digit and a symbol, and complexity rules mostly just push users toward predictable substitutions. Checking the password against a breached-password list (via a k-anonymity range API so the plaintext password never leaves the client) catches more real attacks than a composition rule ever will.
- Hash the password with a slow, memory-hard, salted KDF — Argon2id is the current default recommendation, with bcrypt as a well-understood fallback. Never store a password reversibly, and never use a fast general-purpose hash like SHA-256 or MD5 alone: those are built for speed, which is exactly the wrong property for something an attacker will try to crack offline at billions of guesses per second on cracking hardware.
- Create the account in an unverified state and email a verification link built around a high-entropy, random token — hashed at rest the same way a password-reset token is (more on that below).
- Respond identically regardless of whether the email was already registered — something like "If this email can be registered, we've sent a confirmation" — so the registration endpoint cannot be used to enumerate existing accounts. Rate-limit registration attempts per IP and per email to blunt automated abuse.
#Login
- The client submits credentials over TLS; the server looks up the account by email.
- If the account doesn't exist, the server should still perform a dummy password-hash comparison against a fixed, fake hash before responding, so the response time doesn't leak "this account doesn't exist" through a timing side channel — the same anti-enumeration principle as registration, applied to timing rather than to response content.
- Verify the submitted password against the stored hash using the KDF's own constant-time comparison (Argon2id's and bcrypt's verification functions already do this correctly — never write a manual
==comparison of hash bytes). - Rate-limit and apply escalating backoff keyed by both IP address and account, to blunt credential stuffing and brute force. Account lockout needs care: a fixed lockout after N failures is itself a denial-of-service vector an attacker can point at a victim's account on purpose, so a growing delay (or a CAPTCHA challenge after a threshold) is usually preferable to an outright lock for consumer-facing systems.
- If the password is correct, the account is verified, and any enabled multi-factor step passes, the server issues a session.
#Session and token issuance
There are two dominant approaches, and a senior answer should be able to compare them rather than default to one:
| Server-side session (opaque cookie) | Stateless token (JWT/PASETO) | |
|---|---|---|
| What the client holds | A random, meaningless session ID | A signed token containing claims (sub, exp, iat, scope) |
| How it's validated | Server looks it up in a shared store (Redis/DB) on every request | Server verifies the signature; no lookup needed for the access token itself |
| Revocation | Trivial — delete the server-side record | Hard for the access token itself; solved by keeping access-token TTLs short (5–15 minutes) and revoking the paired refresh token |
| Scaling consideration | Needs a shared, low-latency session store reachable by every instance | Naturally stateless for the access token; the refresh token still needs server-side state to be revocable |
Most production systems land on a hybrid: a short-lived, signed access token for authorizing API calls, paired with a longer-lived refresh token that is tracked server-side (or issued as a rotating opaque value) specifically so it can be revoked. Refresh token rotation — issuing a brand-new refresh token on every use and immediately invalidating the one just used — turns replay of a stolen refresh token into a detectable event: if the old, already-rotated token is ever presented again, that's strong evidence it was stolen, and the whole token family can be revoked on the spot.
Wherever a token is stored also matters. An HttpOnly, Secure, SameSite=Lax (or Strict) cookie can't be read by JavaScript at all, which closes off token theft via XSS — but it needs CSRF protection in exchange, since the browser will attach it automatically. Storing a token in localStorage avoids CSRF but hands it directly to any successful XSS payload; if a system has any script-injection risk at all (and most do), that trade generally favors the cookie.
#Password reset — the step most systems get wrong
Password reset is the highest-risk flow in the whole system, because it is designed to let an unauthenticated party regain access to an account. Every decision here exists to make sure that party is actually the account owner and no one else.
- The request endpoint responds identically whether or not the email exists — the same anti-enumeration principle from registration applies here with even higher stakes, since a reset flow that confirms account existence hands an attacker a free way to build a target list.
- The token itself must be a long, cryptographically random value generated by a CSPRNG — never derived from predictable material like a hash of the user ID and timestamp, which an attacker could potentially reproduce.
- Only a hash of the token is stored server-side, exactly like a password. If the token store ever leaks, the leaked hashes can't be replayed the way raw tokens could — this mirrors why passwords themselves are hashed rather than stored in plaintext.
- The token is short-lived — commonly 15 to 60 minutes — and strictly single-use: the instant it's redeemed, it's deleted or marked used, and any other outstanding reset tokens for that account should be invalidated too.
- Redeeming the token is also a good moment to kill existing sessions. If an attacker already has a live session on the account, forcing a password reset should end their access, not merely add a new password alongside it.
- A "your password was changed" notification email goes out regardless, as an out-of-band signal to the real owner in case the reset wasn't theirs — this is often the first and only warning a victim gets that something happened.
#Tying it together
A senior candidate's real advantage in this question isn't knowing that Argon2id exists — it's noticing that the same handful of principles recur at every stage: never let a response leak whether an account exists, never store a secret (password or token) in a form that can be replayed if it leaks, keep every credential's blast radius small and its lifetime short, and treat the moment of recovery (password reset) as at least as sensitive as the moment of initial authentication, because it's the step an attacker is most likely to target once they've obtained just one piece of the puzzle — a leaked email address, a phished verification code, a stale support ticket.
🧠 Check your understanding
0/3 · 0/3 answered1. Why should a login endpoint perform a dummy password-hash comparison even when the submitted email doesn't match any account?
2. Why is refresh token rotation (issuing a new refresh token on every use and invalidating the old one) valuable beyond just limiting a token's lifetime?
3. Why should a password-reset token be stored server-side only as a hash, never in plaintext?