System DesignIntermediate 15 min Lesson 20 of 42

Authentication — Proving Who You Are

How a system knows who is making a request: password storage done properly, sessions versus tokens, and the trade-offs of each.

System Design · Lesson 20 of 42
0/42 done(0%)

What is it? #

Authentication answers "who are you". Authorization, the next lesson, answers "what may you do". They are different questions and get confused constantly.

The mechanics are straightforward: verify a credential once, then issue something the client presents on later requests, because HTTP is stateless.

That something is either a session ID, which is a reference to server-side state, or a token, which carries the claims itself and is signed.

Password storage is where mistakes are most damaging. Passwords must be hashed with a slow algorithm designed for the purpose, never encrypted and never stored as written.

Think of it like this #

A conference. You show identification once at registration and receive a badge. Nobody re-checks your passport at every room.

A session ID is a badge with a number that staff look up in a list. A token is a badge that carries your details printed on it with a tamper-proof seal — nobody needs to look anything up, but you cannot change what it says until it is reissued.

Simple example #

A user logs in with email and password. The server verifies the hash, issues a session cookie, and every later request carries that cookie. Logging out must actually end the session.

Code #

PYTHON
# --- Password storage ---
import bcrypt      # or argon2-cffi, which is the current preference


def hash_password(plain: str) -> str:
    return bcrypt.hashpw(plain.encode(), bcrypt.gensalt(rounds=12)).decode()


def verify_password(plain: str, stored_hash: str) -> bool:
    return bcrypt.checkpw(plain.encode(), stored_hash.encode())


# NEVER: store plaintext, use md5/sha1/sha256 alone, or encrypt (it is reversible)
PYTHON
# --- Session-based authentication ---
import secrets

SESSION_TTL = 60 * 60 * 24 * 14        # 14 days


def login(email: str, password: str, response) -> bool:
    user = users.find_by_email(email)
    # Compare even when the user is missing, to avoid timing differences
    stored = user.password_hash if user else DUMMY_HASH
    if not verify_password(password, stored) or user is None:
        return False

    session_id = secrets.token_urlsafe(32)          # unguessable
    sessions.set(session_id, {"user_id": user.id}, ttl=SESSION_TTL)
    response.set_cookie(
        "sid", session_id,
        httponly=True,        # JavaScript cannot read it
        secure=True,          # HTTPS only
        samesite="lax",       # limits cross-site sending
        max_age=SESSION_TTL,
    )
    return True


def logout(session_id: str, response) -> None:
    sessions.delete(session_id)                     # actually revoked
    response.delete_cookie("sid")
TEXT
Sessions vs tokens

                    session (cookie)          JWT (token)
state               stored server-side        self-contained, signed
revoke immediately  yes, delete the record    no, until it expires
scaling             needs a shared store      no lookup required
size                small ID                  larger, sent on every request
best for            web apps, browsers        service-to-service, short-lived access

A common, sensible combination: short-lived access token (minutes) plus a
long-lived refresh token stored server-side so it can be revoked.
TEXT
Practices that matter

rate limit login attempts        slows credential stuffing
generic error messages           "invalid email or password", never which one
multi-factor authentication      the single biggest improvement for account safety
password reset tokens            single use, short expiry, invalidate on use
rotate the session ID on login   prevents session fixation
check breached password lists    rather than forcing complex character rules

How it works #

bcrypt.hashpw with a cost factor deliberately takes tens of milliseconds. That is the point: it makes brute-forcing a stolen database expensive. Fast hashes such as SHA-256 are the wrong tool, because they are fast for attackers too.

The salt is generated per password and stored inside the hash string, which is why two identical passwords produce different hashes.

Verifying against a dummy hash when the user does not exist keeps the response time similar either way, so an attacker cannot tell which emails are registered by timing the response.

secrets.token_urlsafe(32) produces a cryptographically random session ID. Anything predictable — a counter, a timestamp, a hash of the email — can be guessed.

The cookie flags do real work. httponly blocks JavaScript access, which limits the damage of a cross-site scripting bug. secure prevents sending over plain HTTP. samesite limits cross-site requests, reducing CSRF exposure.

Logout deletes the server-side record. This is the practical advantage of sessions: revocation is immediate. A JWT remains valid until it expires, which is why access tokens should be short-lived and paired with a revocable refresh token.

Real-world use #

Most web applications use session cookies, because browsers handle them automatically and revocation is simple. APIs and service-to-service calls use tokens.

Delegating to an identity provider — Google, GitHub, or a managed service — removes password storage from your system entirely. For many products that is the right decision, since the safest password database is the one you do not have.

Multi-factor authentication is the highest-value addition by a wide margin. Password reuse means credential stuffing works, and a second factor stops it.

Practical details cause real incidents: password reset tokens that can be reused, sessions that never expire, tokens logged in plaintext, and login endpoints with no rate limit are all common findings in security reviews.

Storing tokens in browser localStorage is a recurring debate. It is readable by any JavaScript on the page, so an httpOnly cookie is generally safer for browser applications.

Common mistakes #

  • Storing passwords in plaintext, encrypted, or hashed with a fast algorithm.
  • Session IDs that are predictable or never expire.
  • Error messages that reveal whether an email is registered.
  • No rate limiting on login, allowing credential stuffing.
  • Assuming a JWT can be revoked — it cannot, until it expires.

Practice #

Implement signup and login with bcrypt hashing, a random session ID with an expiry, and correctly flagged cookies. Add rate limiting after five failed attempts, and make logout actually invalidate the session. Then explain in two sentences what changes if you switch to JWTs.

Quick quiz

  1. 1. Why use bcrypt or argon2 rather than SHA-256 for passwords?

  2. 2. What is the main practical advantage of sessions over JWTs?

  3. 3. What does the httpOnly cookie flag do?

  4. 4. Why use a generic "invalid email or password" message?

  5. 5. What gives the biggest improvement in account security?

Summary

  • Authentication proves identity; issue a session or token afterwards.
  • Hash passwords with a slow algorithm such as bcrypt or argon2.
  • Sessions can be revoked instantly; JWTs cannot until they expire.
  • Set httpOnly, secure and samesite on session cookies.
  • Rate limit logins, use generic errors, and add multi-factor authentication.