Web Foundations & Databases · beginner · ~11 min

Cookies, sessions, and browser storage

- Explain why HTTP is *stateless* and how a session ID lets a server remember a logged-in user across many independent requests. - Trace the full round trip of a session: `Set-Cookie` on login, the browser storing it, and the `Cookie` header on every later request. - Read and reason about the security flags `HttpOnly`, `Secure`, and `SameSite`, and say exactly which attack each one blunts. - Compare where credentials can live — an `HttpOnly` cookie vs. `localStorage` vs. an `Authorization: Bearer` header — and judge the trade-offs. - Recognise session hijacking, session fixation, XSS, and CSRF well enough to spot a missing-flag finding in a security review.

Overview

In How the web works: client, server, request, response you saw that a browser sends a request and the server sends back a response, and that this happens once per page, image, or API call. What that lesson did not stress is that each of those requests stands completely on its own. The server answers request #5 with no memory that it just answered requests #1 through #4 from the same browser a second ago. This property is called being stateless.

Statelessness is great for scaling — any server in a cluster can answer any request — but it creates an obvious problem: if the server forgets you between clicks, how does a site keep you logged in? You type your password once, yet every following page knows who you are. The trick is that the browser quietly re-introduces you on every single request by attaching a small piece of data the server handed it earlier. That piece of data is a cookie, and the identity it points to is a session.

In plain language: on login the server gives your browser a numbered ticket. From then on the browser shows that ticket at the door every time it asks for anything, and the server looks up the ticket number to remember who you are. The formal terms are: the server sends a Set-Cookie header holding a session ID; the browser stores it and returns it in a Cookie header on later requests; the server keeps a session — a record on its side keyed by that ID that says "ticket 8f3a… belongs to user Gil, logged in at 10:02, is an admin."

The catch — and the reason this lesson leans into security even though it is a beginner web topic — is that the ticket is the credential. Anyone who copies your ticket is treated as you until it expires. So the second half of the lesson is about the flags that keep the ticket from being copied.

Why it matters

Almost every site you log into — email, banking, social media, your cloud dashboard — runs on session cookies or bearer tokens. Get them right and users stay safely logged in; get them wrong and attackers log in as those users.

Because the session cookie is a credential, mishandling it is one of the most common and most serious classes of web bug. The same handful of findings appear over and over in real security reviews:

  • A session cookie set without HttpOnly, so any injected JavaScript can read and exfiltrate it.
  • A session cookie set without Secure, so it can be sent in cleartext over plain HTTP and sniffed on a shared network.
  • A session cookie set without SameSite, leaving the site open to CSRF, where another site silently rides your logged-in session.
  • A token dropped into localStorage, which is readable by any script on the page — turning a small XSS bug into full account takeover.
  • Session fixation (the attacker chooses your session ID before you log in) and session hijacking (the attacker steals it after).

Understanding cookies and sessions is therefore not optional trivia — it is the foundation of both building web apps that keep users safe and auditing web apps to check that they do.

Core concepts

1. Stateless HTTP and the session ID

Definition. HTTP is stateless: the protocol carries no built-in memory from one request to the next. A session ID is a large, hard-to-guess random string that the server generates and hands to the browser so that future requests can be tied back to a stored session.

Plain-language explanation. Imagine a coat check. You hand over your coat (log in) and get a numbered tag (the session ID). The coat stays behind the counter (your session lives on the server). Every time you come back you just show the tag, and the attendant fetches your coat. The tag itself is tiny and holds no coat — it is only a pointer to what the counter is storing.

How it works internally. On successful login the server (1) creates a session record in memory, a database, or a cache like Redis; (2) generates a random ID such as s%3A8f3a...; (3) stores the record under that ID; and (4) sends the ID to the browser in a Set-Cookie header. On each later request the browser sends the ID back in a Cookie header, and the server does a lookup: ID → session record → user identity and permissions.

  LOGIN                              LATER REQUEST
  browser  ── POST /login ──▶ server   browser ── GET /dashboard ─▶ server
           ◀ Set-Cookie: sid=8f3a       Cookie: sid=8f3a
                                                        │
           browser stores 8f3a                          ▼
                                              server: lookup[8f3a]
                                              → { user: "Gil", admin: false }

  SERVER-SIDE SESSION STORE
  ┌───────────┬───────────────────────────────┐
  │ sid       │ session data                  │
  ├───────────┼───────────────────────────────┤
  │ 8f3a...   │ user=Gil, loginTime=10:02      │
  │ b21c...   │ user=Sam, loginTime=09:47      │
  └───────────┴───────────────────────────────┘

When to use / when not. Use a server-side session + session-ID cookie for classic web apps where the server renders pages — it is simple and the ID reveals nothing on its own. Reach for stateless tokens (see below) when you have many services that must validate a login without sharing one session store, or a mobile/single-page app talking to an API.

Common pitfall. Using a session ID that is short, sequential, or predictable. If IDs are guessable, an attacker can try other people's IDs and walk into their sessions. Session IDs must be long and cryptographically random.

Knowledge check (explain in your own words): Why can two requests from the same browser, one second apart, not be linked together by the server unless something extra is attached to them?

2. The cookie is a credential

Definition. A cookie is a small name=value pair the browser stores for a site and automatically returns on matching requests. A session cookie is one whose value is (or points to) the session ID.

Plain-language explanation. Because the browser sends the cookie automatically and the server trusts it to identify you, holding the cookie value is exactly as powerful as knowing the password — for as long as the session lasts. This is why we say the session cookie is a credential, not merely contains one.

How it works internally. When the server does lookup[sid] and finds a valid session, it performs the request as that user. It does not re-check the password. So a copied cookie, replayed by anyone, produces the same result. Stealing and reusing a live session cookie is session hijacking.

When to use / when not. Always treat the cookie value as a secret in transit and at rest. Never log it, never put it in a URL (URLs leak via history, referrers, and server logs), never email it.

Common pitfall. Putting the session ID in the URL query string (?sid=8f3a) instead of a cookie. It shows up in browser history, Referer headers sent to other sites, and access logs — all easy places to steal it from.

Knowledge check (concept): A developer says "the cookie is safe because it's just a random number, not the password." Why is that reasoning wrong?

3. The protective flags: HttpOnly, Secure, SameSite

These three attributes are added to the Set-Cookie header. Each one closes a specific hole.

Flag What it does Attack it blunts Cost of omitting it
HttpOnly Hides the cookie from JavaScript (document.cookie can't see it) XSS-based cookie theft Any injected script can read and send the cookie away
Secure Sends the cookie only over HTTPS Network sniffing / man-in-the-middle Cookie can travel in cleartext and be captured
SameSite=Lax or Strict Restricts sending the cookie on cross-site requests CSRF (cross-site request forgery) Another site can trigger authenticated actions

How they work internally. HttpOnly is enforced by the browser: the cookie exists in the cookie jar and is attached to requests, but document.cookie simply omits it. Secure makes the browser refuse to attach the cookie to a plain http:// request. SameSite=Lax tells the browser to send the cookie on top-level navigations to your site but not on background cross-site requests like an image load or form POST originating from evil.com; Strict is even tighter and withholds it on cross-site navigations too.

When to use / when not. For a session cookie, set all three unless you have a concrete reason not to. Use SameSite=Strict for high-value apps (banking); Lax is the common default that still allows normal inbound links to work. Only drop Secure on localhost during development.

Common pitfall. Assuming HttpOnly stops XSS. It does not stop the script from running or from performing actions as you while the page is open — it only stops the script from reading the cookie value to use later or elsewhere. Defence in depth (fix the XSS too) still matters.

Set-Cookie: sid=8f3a; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
            └─┬──┘   └──┬───┘ └─┬──┘ └────┬────┘ └─┬─┘ └────┬─────┘
            value   no JS    HTTPS   cross-site  scope   expiry
                    access   only    limit

Knowledge check (find-the-bug): A login response sets Set-Cookie: sid=8f3a; Path=/. List the three flags that are missing and name the attack each omission enables.

4. localStorage, sessionStorage, and where credentials live

Definition. localStorage and sessionStorage are simple key/value stores the browser gives to JavaScript. localStorage persists until cleared; sessionStorage lasts until the tab closes.

Plain-language explanation. Both are fully readable by any JavaScript running on the page. There is no HttpOnly equivalent. So if an attacker can inject a single line of script (an XSS bug), they can read everything in localStorage — including any token you parked there.

When to use / when not. Fine for non-secret UI state (theme, last-opened tab, a draft). Avoid it for anything that is a credential. An HttpOnly cookie is the safer home for a session identifier precisely because script can't read it.

Storage Readable by JS? Sent automatically? Good for a credential?
HttpOnly cookie No Yes (per flags) Yes
Non-HttpOnly cookie Yes Yes Weaker
localStorage Yes No No
sessionStorage Yes No No

Common pitfall. "We store the JWT in localStorage so it survives refresh." Convenient, but one XSS bug now equals full account takeover, because the attacker reads the token directly.

5. Bearer tokens

Definition. A bearer token (often a JWT) is a credential sent in an Authorization: Bearer <token> header rather than in a cookie. "Bearer" is the whole security model: whoever bears (holds) the token is treated as the user — exactly like the session cookie.

Plain-language explanation. Instead of the browser automatically attaching a cookie, your JavaScript explicitly adds the header on each API call. This gives you control (handy for APIs and mobile apps) but also responsibility: you must store the token somewhere and choose that somewhere wisely (see the previous concept).

When to use / when not. Bearer tokens shine for APIs consumed by many clients and for stateless verification across services. For a plain server-rendered website, a session cookie is usually simpler and, with HttpOnly, safer by default.

Common pitfall. Because bearer tokens are not sent automatically by the browser, they are naturally immune to CSRF — but developers sometimes store them in localStorage and re-open the XSS door they thought they closed. The storage choice, not the token format, decides the risk.

Knowledge check (concept): A session cookie is auto-sent by the browser, while a bearer token in an Authorization header is added manually by your code. Which of the two is naturally more exposed to CSRF, and why?

Syntax notes

There is no C syntax here — this is HTTP and browser behaviour. The pieces to recognise are three HTTP headers and two JavaScript APIs.

Server sets a cookie (response header):

Set-Cookie: sid=8f3a9c...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
  • sid=8f3a9c... — the cookie name and value (the session ID).
  • HttpOnly; Secure; SameSite=Lax — the three protective flags.
  • Path=/ — which URLs the cookie applies to.
  • Max-Age=3600 — lifetime in seconds (here, one hour).

Browser returns it (request header):

Cookie: sid=8f3a9c...

Note the browser sends back only name=value — the flags are instructions to the browser, not data for the server.

Bearer token instead of a cookie (request header):

Authorization: Bearer eyJhbGciOiJIUzI1NiInR5cCI6...

Browser storage from JavaScript:

localStorage.setItem("theme", "dark");   // fine: not a secret
const t = localStorage.getItem("theme"); // any script on the page can read this
// document.cookie shows ONLY non-HttpOnly cookies:
console.log(document.cookie);            // sid is absent if it is HttpOnly

Lesson

HTTP is stateless — each request is handled on its own, with no memory of the requests before it. So how does a site remember that you are logged in? With cookies.

Cookies and sessions

When you log in, the server sends back a Set-Cookie header containing a session identifier. The browser stores it and returns it on every later request.

The server keeps a matching session on its side. It maps the session ID to who you are and what you are allowed to do.

In effect, the session cookie is your temporary password. Stealing it means impersonating you — this is called session hijacking.

Protective cookie flags (audit these)

A session cookie should set these flags:

  • HttpOnly — JavaScript cannot read the cookie. This blunts theft through XSS (cross-site scripting, where an attacker runs script in your browser).
  • Secure — the cookie is sent only over HTTPS, never in cleartext.
  • SameSite (Lax or Strict) — limits when the cookie is sent on cross-site requests, which helps against CSRF (cross-site request forgery).

A session cookie missing these flags is a standard finding in a security review.

Browser storage

  • localStorage / sessionStorage — simple key/value stores that JavaScript can read.

They are convenient, but anything in localStorage is exposed to XSS. Storing a token there is riskier than keeping it in an HttpOnly cookie, which scripts cannot read.

Tokens

Some apps use bearer tokens (for example, a JWT) instead of session cookies. The token is sent in an Authorization header.

The rule is the same as for cookies: whoever holds the token is treated as the user.

Code examples

The example below is a tiny, realistic login server written with Node.js and Express — the kind of code that actually issues the headers we have been discussing. It shows a secure session cookie being set on login, checked on a protected route, and cleared on logout.

// server.js  —  run with:  node server.js   (needs: npm install express cookie-parser)
const express = require("express");
const crypto = require("crypto");
const cookieParser = require("cookie-parser");

const app = express();
app.use(express.urlencoded({ extended: false })); // parse form POST bodies
app.use(cookieParser());

// Pretend user database. NEVER store plaintext passwords in real code.
const USERS = { gil: "hunter2" };

// Server-side session store: sessionId -> { user, created }
const sessions = new Map();

// Create a large, cryptographically random session ID (unguessable).
function newSessionId() {
  return crypto.randomBytes(32).toString("hex");
}

app.post("/login", (req, res) => {
  const { user, pass } = req.body;
  if (!user || USERS[user] !== pass) {
    return res.status(401).send("Invalid credentials"); // fail closed
  }

  const sid = newSessionId();
  sessions.set(sid, { user, created: Date.now() }); // store session on the server

  // Set the session cookie with ALL THREE protective flags.
  res.cookie("sid", sid, {
    httpOnly: true,       // JS cannot read it -> blunts XSS theft
    secure: true,         // HTTPS only -> blunts sniffing (use false on localhost)
    sameSite: "lax",      // limits cross-site sending -> blunts CSRF
    maxAge: 60 * 60 * 1000 // 1 hour, in milliseconds
  });
  res.send("Logged in");
});

// Middleware: turn the cookie back into a user, or reject.
function requireLogin(req, res, next) {
  const sid = req.cookies.sid;
  const session = sid && sessions.get(sid);
  if (!session) return res.status(401).send("Please log in");
  req.currentUser = session.user;
  next();
}

app.get("/dashboard", requireLogin, (req, res) => {
  res.send(`Welcome back, ${req.currentUser}`);
});

app.post("/logout", (req, res) => {
  const sid = req.cookies.sid;
  if (sid) sessions.delete(sid); // invalidate on the server side
  res.clearCookie("sid");        // tell the browser to drop it
  res.send("Logged out");
});

app.listen(3000, () => console.log("Listening on http://localhost:3000"));

What it does. POST /login checks the password, creates a random session ID, stores a session record under it, and returns a hardened Set-Cookie. GET /dashboard is protected by requireLogin, which converts the incoming cookie back into a user or returns 401. POST /logout deletes the server-side session and clears the cookie, so a stolen cookie stops working immediately.

Expected behaviour. After a correct login the response carries a header like Set-Cookie: sid=<64 hex chars>; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax. A later GET /dashboard that includes Cookie: sid=<same> returns Welcome back, gil. The same request with no cookie, or a made-up sid, returns 401 Please log in.

Key edge cases. With secure: true the cookie will not be set over plain http://localhost, so during local testing over HTTP you would temporarily set secure: false. If the server restarts, the in-memory sessions map is wiped and every user is logged out — real apps use a shared store (Redis, a database) so sessions survive restarts and scale across multiple servers.

Line by line

We follow one successful login and the dashboard request that comes after it.

  1. app.use(express.urlencoded(...)) and cookieParser() register middleware so that req.body holds the submitted form fields and req.cookies holds the incoming cookies. Without these, req.body.user and req.cookies.sid would be undefined.
  2. POST /login arrives with body { user: "gil", pass: "hunter2" }. The guard USERS[user] !== pass compares the stored password to the submitted one. On a mismatch it returns 401 and stops — failing closed.
  3. newSessionId() calls crypto.randomBytes(32) — 32 bytes = 256 bits of randomness — and hex-encodes it to a 64-character string. This is the unguessable ticket number.
  4. sessions.set(sid, { user, created }) writes the server-side record. After this line the store maps sid -> { user: "gil", created: <timestamp> }.
  5. res.cookie("sid", sid, {...}) builds the Set-Cookie response header with all three flags. The browser now owns a copy of sid; the server owns the matching record.
  6. Browser receives the response, stores the cookie in its cookie jar, and — because of HttpOnly — refuses to expose it to any page script.
  7. Later, GET /dashboard fires. The browser automatically attaches Cookie: sid=<64 hex>. requireLogin reads req.cookies.sid, then sessions.get(sid) looks the record up.
  8. Lookup succeeds, so req.currentUser = "gil" and next() passes control to the route handler, which replies Welcome back, gil. Had the lookup failed, the middleware would have returned 401 before the handler ran.
Step req.cookies.sid sessions.get(sid) Response
Login success (being set) just created Set-Cookie + "Logged in"
Dashboard, valid cookie 8f3a… { user: "gil" } "Welcome back, gil"
Dashboard, no cookie undefined undefined 401
Dashboard, forged sid deadbeef… undefined 401
After logout 8f3a… deleted → undefined 401

Common mistakes

Mistake 1 — Forgetting the flags.

res.cookie("sid", sid);            // WRONG: no HttpOnly, Secure, or SameSite

Why it's wrong: script can read it (XSS theft), it can travel over plain HTTP (sniffing), and other sites can ride it (CSRF). Corrected:

res.cookie("sid", sid, { httpOnly: true, secure: true, sameSite: "lax" });

How to catch it: inspect the response in your browser's Network tab and confirm the Set-Cookie line lists all three flags.

Mistake 2 — Storing the token in localStorage "to survive refresh."

localStorage.setItem("token", jwt);         // WRONG: readable by any script

Why it's wrong: a single XSS bug lets the attacker run localStorage.getItem("token") and walk off with the credential. Corrected approach: keep the session identifier in an HttpOnly cookie so page scripts cannot read it at all. Recognise it by grepping the frontend for localStorage near auth code.

Mistake 3 — Predictable or reused session IDs.

let counter = 0;
const sid = "user-" + (++counter);          // WRONG: guessable and sequential

Why it's wrong: an attacker guesses user-2, user-3, … and hijacks other sessions. Corrected: crypto.randomBytes(32).toString("hex"). Prevent it by never generating IDs from counters, timestamps, or usernames.

Mistake 4 — Logout that only clears the cookie.

res.clearCookie("sid");                      // WRONG on its own

Why it's wrong: if the attacker already copied the cookie, clearing the browser's copy does nothing — the session is still valid on the server. Corrected: also sessions.delete(sid) so the ID is dead everywhere. Recognise it by asking "does logout invalidate server-side state, or just the browser?"

Mistake 5 — Session fixation. Why it's wrong: if you accept a session ID supplied by the client and keep using it after login, an attacker can plant a known ID, wait for the victim to log in on it, then reuse it. Corrected: generate a fresh session ID at the moment of successful login (as the sample does) and discard any pre-login ID.

Debugging tips

"I logged in but the next page says I'm logged out."

  • Open the browser Network tab, click the login response, and check for a Set-Cookie header. If it's missing, the server never set the cookie.
  • If it's present but the next request has no Cookie header, the cookie was rejected. Two frequent causes: Secure is set but you're on plain http:// (the browser silently drops it), or the Path/Domain doesn't match the URL you're visiting.
  • SameSite=Strict can also surprise you: following a link from another site won't send the cookie, so the first page load looks logged-out.

"document.cookie doesn't show my session cookie."

  • That's correct and expected when HttpOnly is set — it is not a bug. Verify the cookie exists via the browser's Application/Storage tab, which shows HttpOnly cookies that document.cookie hides.

"Everyone got logged out after I deployed."

  • An in-memory session store is wiped on every restart. Use a shared store (Redis/DB) so sessions persist and are shared across instances.

Logic errors to check. Fail closed, not open: if the session lookup errors or returns nothing, deny access rather than defaulting to "allow." Confirm your 401 path actually returns before the protected handler runs.

Questions to ask when it doesn't work. Is the cookie being set? Is it being sent back? Does the server find a session for that ID? Does Secure conflict with an http:// test URL? Did logout invalidate the server session or only the browser copy?

Memory safety

This is a web/security concept lesson, not C, so the concern is not memory bounds but credential safety and robustness. The following are defensive practices; where a weakness is shown it is labelled and paired with its fix.

  • Treat the session ID as a secret. Weakness: logging the cookie value, or putting it in a URL. Fix: keep it only in the cookie jar and the server store; never log, print, or URL-encode it.
  • Generate unguessable IDs. Weakness (session prediction): short/sequential IDs let attackers guess valid sessions. Fix: at least 128 bits of cryptographic randomness (crypto.randomBytes(32)).
  • Harden the cookie. Weakness: missing HttpOnly/Secure/SameSite. Fix: set all three on every session cookie; this is the single most common review finding.
  • Rotate on privilege change. Weakness (session fixation): reusing a pre-login ID. Fix: issue a brand-new session ID at login and after any privilege escalation, and expire old ones.
  • Fail closed and validate input. Weakness: trusting a client-supplied session ID or defaulting to "allow" on lookup failure. Fix: validate the ID against the server store and deny by default.
  • Least privilege and short lifetimes. Fix: give sessions a sensible expiry (Max-Age), invalidate them fully on logout (server + browser), and don't store more in the session than the request needs.
  • Note on localStorage: it offers no isolation from scripts, so it is the wrong place for any credential. Prefer an HttpOnly cookie. All examples here are lab/localhost only — never test against systems you don't own.

Real-world uses

Concrete use case. Every session-based web app you use — your webmail, your bank's dashboard, a project tool like GitHub or Jira — issues a session cookie exactly like the one in the example. GitHub's login, for instance, returns an HttpOnly; Secure; SameSite=Lax session cookie; your browser replays it on each page so you stay signed in, and "Sign out" invalidates it server-side. API platforms (Stripe, Twilio, cloud providers) instead hand out bearer tokens you place in an Authorization header, following the same holder-equals-user rule.

Beginner best-practice habits.

  • Always set HttpOnly, Secure, and SameSite on session cookies; verify them in the Network tab.
  • Never put credentials in URLs or localStorage.
  • Use a well-tested session library rather than hand-rolling ID generation.
  • Give sessions an expiry and make logout invalidate the server-side session.

Advanced best-practice habits.

  • Store sessions in a shared, restartable store (Redis/DB) so they survive deploys and scale horizontally.
  • Rotate session IDs on login and privilege change to defeat fixation; consider idle and absolute timeouts.
  • Bind sensitive actions to an extra CSRF token or SameSite=Strict, and add re-authentication for high-risk operations.
  • Monitor for anomalies (a session ID suddenly used from a new IP/user-agent) and support remote session revocation ("log out all devices").
  • When auditing, always confirm the flags, ID randomness, logout invalidation, and storage location as a standard checklist.

Practice tasks

Beginner 1 — Read a Set-Cookie. Given the header Set-Cookie: sid=8f3a; HttpOnly; Secure; SameSite=Lax; Max-Age=3600, write down (a) the cookie name and value, (b) which requests the browser will send it on, and (c) how long it lasts. Concepts: cookie structure, flags. Hint: the browser sends back only name=value.

Beginner 2 — Spot the missing flags. You see Set-Cookie: sid=8f3a; Path=/ on a login response. List every protective flag that is missing and, for each, name the attack it would have blunted. Concepts: HttpOnly, Secure, SameSite, XSS, sniffing, CSRF. Hint: there are exactly three.

Intermediate 1 — Add a middleware guard. Starting from the lesson's server, add a POST /change-email route that is protected by requireLogin and updates an in-memory emails map for req.currentUser. Requirements: reject unauthenticated requests with 401; only let a user change their own email. Input/output: an authenticated POST /change-email with body email=new@x.com returns 200 Updated; the same request with no cookie returns 401. Concepts: session lookup, authorization, fail-closed. Hint: reuse req.currentUser.

Intermediate 2 — Implement session expiry. Extend the store so each session records created, and make requireLogin reject (and delete) any session older than one hour. Requirements: an expired session behaves exactly like no session; deletion happens on access. Input/output: a request with a 2-hour-old sid returns 401 and the session is removed from the store. Concepts: server-side sessions, timeouts. Hint: compare Date.now() - session.created.

Challenge — Prevent session fixation. Modify the login flow so that any session ID present before login is discarded and a brand-new ID is issued only on successful authentication; verify that an attacker-planted sid set before login is not honoured afterward. Requirements: pre-login and post-login IDs must differ; the old ID must be invalidated in the store. Concepts: session fixation, ID rotation, fail-closed. Hint: generate the new ID inside the success branch and delete any old entry — do not reuse whatever the client sent.

Summary

HTTP is stateless: every request stands alone, so a site keeps you logged in by giving your browser a session ID in a Set-Cookie header and looking it up in a server-side session on each later request. That cookie is a credential — whoever holds it is treated as you, so stealing it is session hijacking.

The most important syntax is the hardened cookie: Set-Cookie: sid=…; HttpOnly; Secure; SameSite=Lax. HttpOnly hides it from JavaScript (blunts XSS theft), Secure keeps it on HTTPS (blunts sniffing), and SameSite limits cross-site sending (blunts CSRF). Bearer tokens in an Authorization header follow the same holder-equals-user rule, and localStorage is readable by any script, so it is the wrong home for a credential.

Common mistakes: omitting the flags, using guessable session IDs, parking tokens in localStorage, logging out only in the browser, and reusing a pre-login ID (session fixation). Remember: generate long random IDs, set all three flags, prefer HttpOnly cookies over localStorage, fail closed, and make logout invalidate the session on the server, not just the browser.

Practice with these exercises