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

  1. POST the credentials. The browser sends {email, password} to /login.
  2. The server verifies them against the stored password hash.
  3. The server creates a session. It generates a long random id and saves sid → {userId: 42, createdAt, ip} in Redis or a table.
  4. The server replies with Set-Cookie. The response carries Set-Cookie: sid=8f2b…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800.
  5. The browser stores it in its cookie jar, keyed by host. JavaScript cannot read it because of HttpOnly.
  6. 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.
  7. The server looks it up, finds userId: 42, and the handler runs as that user.
  8. Logout deletes the record and sends an expired Set-Cookie to clear the jar.
Cookie session login and subsequent requestsBrowserAPI serverSession storePOST /login {email, password}store sid → user 42Set-Cookie: sid=8f2b…; HttpOnlyGET /api/orders — Cookie: sid=8f2b…(attached automatically)look up sid200 OK — orders of user 42
Steps 4–6 are the whole point: the browser does the credential handling, not your code.

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

Example · http
# 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.

Example · javascript
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.

Example · bash
# -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

  • Be the first to comment on this lesson.