What Is It?
Authentication is the process of proving who you are to a software system. At its core, you present something that only you should have (a password, a fingerprint, a token), the system verifies it, and then it needs some way to remember that it verified you — because HTTP is stateless and doesn't have memory between requests.
That last part is the engineering challenge. Verifying a password once is straightforward. But HTTP forgets after every request. When you hit "Add to Cart" after logging in, how does the server know it's you and not a random request? This is what sessions and tokens solve — and understanding how they differ explains dozens of design decisions in web auth.
Authentication is separate from authorisation: authentication is "who are you?", authorisation is "what are you allowed to do?". You can be authenticated (we know you're Jamie) but not authorised (Jamie isn't allowed to access this admin page). Both matter, but they're different problems.
How It Actually Works
The Login Flow
When you log in to a website:
- You POST your credentials to the server (
POST /auth/loginwith{email, password}) - The server fetches your user record from the database
- It verifies your password by hashing your input and comparing it to the stored hash (never stores plain text)
- If it matches, the server needs to establish a way for future requests to be recognised as yours
- It returns either a session cookie or a token
Option A: Session-Based Authentication
Browser Server Database
| | |
|──── POST /login ─────────────►| |
| {email, password} |──── find user ──────────►|
| |◄─── user record ─────── |
| | (verify password hash) |
| |──── create session ─────►|
| | sessions table: |
| | id=abc123, userId=1 |
|◄─── 200 OK ──────────────────| |
| Set-Cookie: session=abc123| |
| | |
|──── GET /dashboard ──────────►| |
| Cookie: session=abc123 |──── lookup session ─────►|
| |◄─── userId=1 ─────────── |
|◄─── 200 (dashboard HTML) ─── | |
The session ID (abc123) is an opaque random string stored in a cookie. The server stores the session in its database (or Redis). Every request, the server looks up the session to find who the user is.
Why sessions: The server controls everything. Want to invalidate a session? Delete it from the database. Logout works instantly. The cookie contains no information about the user — just a lookup key.
Why not sessions: State. Every server in your cluster needs access to the session store (requires Redis or a database). Scales horizontally but adds infrastructure complexity.
Option B: Token-Based Authentication (JWT)
Browser Server
| |
|──── POST /login ─────────────►|
| {email, password} | (verify password)
| | (create and sign JWT)
|◄─── 200 OK ──────────────────|
| { token: "eyJhbG..." } |
| |
|──── GET /dashboard ──────────►|
| Authorization: Bearer | (verify JWT signature)
| eyJhbG... | (extract userId from payload)
|◄─── 200 (dashboard data) ─── |
A JWT (JSON Web Token) is a signed token that contains the user's identity in its payload. The server signs it with a secret key. On subsequent requests, the server verifies the signature — no database lookup needed. The token contains the data.
Why tokens: Stateless. Any server can verify the signature without a central session store. Works well for APIs consumed by mobile apps and SPAs. Works for cross-domain authentication.
Why not tokens: You can't invalidate them before expiry. If a JWT is stolen or you want to log a user out immediately, you can't — it's valid until expiry (unless you build a token revocation list, which makes them stateful again). Short expiry + refresh tokens is the common mitigation.
Cookies vs LocalStorage
Where do you store the token on the client?
| Cookie (HttpOnly) | LocalStorage | |
|---|---|---|
| Accessible to JS | No | Yes |
| Sent automatically | Yes (with every request) | No (must add header) |
| XSS protection | ✓ (JS can't read it) | ✗ (JS can read it) |
| CSRF protection | Needs token or SameSite | Not needed (not auto-sent) |
| Best for | Session cookies, auth tokens | Non-sensitive data |
HttpOnly cookies cannot be read by JavaScript — they're invisible to document.cookie. This is crucial: if your site has an XSS vulnerability and an attacker injects JavaScript, they can't steal HttpOnly cookies. They can steal anything in LocalStorage.
What Happens On Every Request After Login
This is the part many people don't think about:
- Browser sends cookie (or Authorization header with token) with every request — automatically for cookies, or via your fetch headers for tokens
- Server middleware runs before your route handler
- Middleware reads the cookie/token
- Session: looks up session ID in database → gets userId
- Token: verifies signature → extracts userId from payload
- Attaches user object to request:
req.user = { id: 1, email: "jamie@..." } - Your route handler can now access
req.user - Route handler checks authorisation: is this user allowed to do this?
If step 3 fails (no cookie/token), middleware returns 401 Unauthorized. If step 8 fails (user doesn't have permission), it returns 403 Forbidden.
The Jargon Decoded
- Authentication (authn) — Proving who you are. Login is authentication.
- Authorisation (authz) — Determining what you're allowed to do. Role checks are authorisation.
- Session — Server-side state linking a session ID (stored in cookie) to a user. Stored in database or Redis.
- JWT (JSON Web Token) — A signed token containing user data. No server-side storage required. More in the OAuth & JWT article.
- Cookie — A small piece of data stored in the browser, sent automatically with every request to the same domain.
HttpOnlycookies can't be read by JavaScript. - Password Hash — Passwords should never be stored in plaintext. They're hashed with bcrypt or Argon2 — one-way functions that can't be reversed. Verification hashes the input and compares.
- Salt — A random string added to a password before hashing so two users with the same password have different hashes. Prevents rainbow table attacks.
- CSRF (Cross-Site Request Forgery) — An attack where a malicious site tricks your browser into making authenticated requests to another site (using your cookies). Mitigated by
SameSitecookie attribute or CSRF tokens. - XSS (Cross-Site Scripting) — An attack where malicious JavaScript runs on your site, potentially stealing tokens from LocalStorage or cookies without
HttpOnly. - Refresh Token — A long-lived token used to obtain new short-lived access tokens, without re-entering credentials. Stored in HttpOnly cookie.
Why This Matters When You're Building
Using an auth library (Clerk, Auth0, NextAuth) is not understanding auth. These libraries abstract away the implementation — which is appropriate. But when something breaks (login isn't working, users get randomly logged out, "invalid session" errors), you need to understand what's underneath.
Password storage is non-negotiable. Never store passwords in plaintext or with weak hashing (MD5, SHA1). Use bcrypt (via libraries like bcryptjs) or Argon2. Every auth library does this correctly for you — if you're rolling your own, this is the most critical thing to get right.
HttpOnly cookies for auth tokens. If you're storing JWTs in LocalStorage, you're one XSS vulnerability away from every user's token being stolen. Store auth tokens in HttpOnly cookies. Yes, you need to handle CSRF (use SameSite=Strict or SameSite=Lax).
Logout must invalidate server-side. If you're using sessions, delete the session from the database on logout. If you're using JWTs, this is harder — the token is still valid until expiry. Short-lived tokens (15 minutes) + refresh tokens is the standard pattern.
What To Tell The AI
"I'm building authentication from scratch in Next.js without a library. Walk me through: hashing passwords with bcrypt, creating and storing sessions in a database, setting HttpOnly cookies, middleware that validates sessions on every request, and a logout endpoint that invalidates the session."
"My app uses JWTs stored in LocalStorage. Security-review this approach, explain the XSS risk specifically, and refactor to use HttpOnly cookies instead — show the changes needed in the API responses and the frontend fetch calls."
"Implement auth middleware for my Express API that: checks for a Bearer token in Authorization header, verifies the JWT signature, attaches the decoded user to req.user, and returns 401 if missing/invalid. Include the error cases."
"I'm using NextAuth with GitHub OAuth. A user logs in, then I revoke their access in GitHub — but they can still use my app. Explain why this happens and what options I have to force re-validation."
"My users are getting randomly logged out. I use JWT access tokens that expire after 15 minutes and refresh tokens after 7 days. Walk me through the refresh token flow — how the frontend should detect an expired access token and silently refresh it."
Common Misconceptions
"Hashed passwords are safe if the database leaks." Hashing protects against trivial exposure, but weak hashing (MD5, unsalted SHA1) can be reversed with rainbow tables or brute force. Always use bcrypt or Argon2, which are designed to be slow and include salting. The slowness is a feature, not a bug — it makes brute force computationally expensive.
"HTTPS means I don't need to worry about token security." HTTPS encrypts the connection but doesn't protect tokens from being stolen by malicious JavaScript on the page (XSS) or from CSRF attacks. HTTPS and token security are orthogonal concerns.
"JWTs are more secure than sessions." Security is about implementation, not the technology. A properly implemented session (random ID, server-side store, HttpOnly cookie) is just as secure as a properly implemented JWT. JWTs have the advantage of statelessness; sessions have the advantage of instant revocation. Choose based on your requirements.
"Auth libraries handle everything." Libraries like NextAuth, Clerk, and Auth0 handle the common cases. But they don't know your business logic: what roles do you have? Which pages require what permissions? What happens when a user is suspended mid-session? You still need to implement authorisation — the library only handles authentication.
Sources
- OWASP Authentication Cheat Sheet — Security-focused best practices for auth implementation
- Auth0 — Sessions and Cookies — Clear explanation of session-based auth patterns
- Understanding JWT — Official introduction to JSON Web Tokens with an interactive decoder
- Password Storage Cheat Sheet — OWASP — Why bcrypt/Argon2, what work factor to use
- The Copenhagen Book — Comprehensive guide to web authentication patterns, free and recently updated