How Cookie Sessions Work, Step by Step
Follow one login from the form submit to the third authenticated request, and see exactly what the browser does for free.
Cookie authentication is the browser's native answer to a stateless protocol. The server hands the browser a small value; the browser stores it and re-sends it automatically on every subsequent request to that host. Your JavaScript does not have to remember anything.
The full sequence
- POST the credentials. The browser sends
{email, password}to/login. - The server verifies them against the stored password hash.
- The server creates a session. It generates a long random id and saves
sid → {userId: 42, createdAt, ip}in Redis or a table. - The server replies with
Set-Cookie. The response carriesSet-Cookie: sid=8f2b…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800. - The browser stores it in its cookie jar, keyed by host. JavaScript cannot read it because of
HttpOnly. - Every later request carries it.
Cookie: sid=8f2b…is attached by the browser, on fetch calls, on image loads, on form posts — on everything going to that host. - The server looks it up, finds
userId: 42, and the handler runs as that user. - Logout deletes the record and sends an expired
Set-Cookieto clear the jar.
The session id itself
It must be unguessable — at least 128 bits from a CSPRNG. It must be meaningless: no user id, no email, no base64 of anything. And it is a bearer credential in the literal sense: whoever holds it is the user, which is why HttpOnly and Secure are not optional.
The catch
"The browser attaches it automatically" is both the feature and the vulnerability. A malicious page on another site can cause a request to your API, and the browser will helpfully attach the cookie to it. That is CSRF, and it is why cookie auth always ships with a second defence.
Example
# 1. Login
POST /login HTTP/1.1
Host: dfg.com
Content-Type: application/json
{"email":"[email protected]","password":"s3cret"}
# 2. Response hands over the session
HTTP/1.1 200 OK
Set-Cookie: sid=8f2bd1c94a7e...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
Content-Type: application/json
{"user":{"id":42,"email":"[email protected]"}}
# 3. Every later request — the browser adds this line itself
GET /api/orders HTTP/1.1
Host: dfg.com
Cookie: sid=8f2bd1c94a7e...When to use it
- A server-rendered dashboard uses session cookies so that a full page load is already authenticated with no JavaScript involved.
- A team keeps sessions in Redis so that 'log out everywhere' is a single pattern delete and takes effect on the next request.
- An app stores the login IP and user agent alongside the session so an unexpected change can force re-authentication.
More examples
Generating and storing a session
Hashing the session id before storing it is the same reasoning as hashing an API key: read access to the store should not hand someone a working credential.
import { randomBytes, createHash } from 'crypto';
const SESSION_TTL_S = 60 * 30; // 30 minutes idle
export async function createSession(userId, req) {
const sid = randomBytes(32).toString('base64url'); // 256 bits, unguessable
// Store a HASH of the id: a leaked Redis dump then contains no usable cookies.
const key = 'sess:' + createHash('sha256').update(sid).digest('hex');
await redis.set(key, JSON.stringify({
userId,
createdAt: Date.now(),
ip: req.ip,
ua: req.get('user-agent'),
}), 'EX', SESSION_TTL_S);
return sid; // only this goes into the cookie
}
export async function readSession(sid) {
if (!sid) return null;
const key = 'sess:' + createHash('sha256').update(sid).digest('hex');
const raw = await redis.get(key);
if (!raw) return null;
await redis.expire(key, SESSION_TTL_S); // sliding idle timeout
return JSON.parse(raw);
}Watching it happen with curl
curl's cookie jar is the fastest way to reproduce a browser session bug without opening devtools — and it makes the automatic re-sending obvious.
# -c writes the cookie jar, -b sends it — curl imitating a browser
curl -c jar.txt -X POST https://dfg.com/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"s3cret"}'
cat jar.txt
# dfg.com FALSE / TRUE 1754314200 sid 8f2bd1c94a7e...
# The second request needs no credentials of its own
curl -b jar.txt https://dfg.com/api/orders
# Drop the jar and you are anonymous again
curl https://dfg.com/api/orders
# → 401
Discussion