# Cookies and Sessions

Course: [Express.js](https://www.devtutorials.ai/learn/expressjs) · Section: Data Handling
Source: https://www.devtutorials.ai/learn/expressjs/data-handling/cookies-and-sessions

> How cookies travel, setting, reading, signing and clearing cookies with cookie-parser, cookie options, sessions with express-session, session stores, login with hashed passwords, protecting page and API routes, logout, and session security.

## Cookies and Sessions Introduction

HTTP forgets everything between requests. Each request arrives as a stranger.

- **Cookie** = a small name=value note the server asks the browser to keep.
- The browser sends the cookie back with every later request to that site.
- **Session** = data the server keeps about one visitor, found by an ID in a cookie.
- Cookies hold small, harmless things, like a theme.
- Sessions hold important things, like who is logged in.

**Simple lesson:** Cookies let the server recognise a returning visitor.

### A Real-World Example

Think about the school library.

1. On your first visit, the librarian gives you a library card with a number.
2. You show the card on every visit.
3. The librarian looks up your number in the register: name, class, books borrowed.
4. The card only holds a number. The register holds the details.

The card is the **session cookie**. The register is the **session store**.

In this lesson, you will learn:

1. How cookies travel in `Set-Cookie` and `Cookie` headers.
2. How to set, read, sign and clear cookies.
3. The cookie options that keep users safe.
4. How sessions work with express-session.
5. How to log in, protect routes and log out.
6. How to keep sessions secure.

## How Cookies Travel

<figure className="xfig ngc-fig">
<svg viewBox="0 0 560 190" role="img" aria-labelledby="xfig-cookie" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
  <title id="xfig-cookie">Animation: the server sends Set-Cookie once, and the browser sends the Cookie header back on every later request</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>
    <rect x="15" y="20" width="110" height="150" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
    <text x="70" y="45" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>Browser</text>
    <rect x="30" y="120" width="80" height="34" rx="6" style={{ fill: "var(--panel)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}>
      <animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.3;0.32;1" dur="8s" repeatCount="indefinite" />
    </rect>
    <text x="70" y="142" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 11 }} opacity="0">theme=dark<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.3;0.32;1" dur="8s" repeatCount="indefinite" /></text>
    <rect x="435" y="20" width="110" height="150" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
    <text x="490" y="45" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>Express</text>
  </g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <g opacity="0">
      <rect x="-90" y="-12" width="180" height="22" rx="11" style={{ fill: "var(--ngc-green)" }} />
      <text x="0" y="3" textAnchor="middle" style={{ fill: "#fff" }}>Set-Cookie: theme=dark</text>
      <animateMotion path="M370 70 L190 70" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.05;0.3;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.05;0.3;0.32;1" dur="8s" repeatCount="indefinite" />
    </g>
    <g opacity="0">
      <rect x="-80" y="-12" width="160" height="22" rx="11" style={{ fill: "var(--ngc-blue)" }} />
      <text x="0" y="3" textAnchor="middle" style={{ fill: "#fff" }}>Cookie: theme=dark</text>
      <animateMotion path="M190 110 L370 110" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1;1" keyTimes="0;0.4;0.62;0.64;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.39;0.4;0.62;0.64;1" dur="8s" repeatCount="indefinite" />
    </g>
    <g opacity="0">
      <rect x="-80" y="-12" width="160" height="22" rx="11" style={{ fill: "var(--ngc-blue)" }} />
      <text x="0" y="3" textAnchor="middle" style={{ fill: "#fff" }}>Cookie: theme=dark</text>
      <animateMotion path="M190 145 L370 145" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.7;0.92;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.69;0.7;0.92;1" dur="8s" repeatCount="indefinite" />
    </g>
  </g>
  <g style={{ fontFamily: "var(--sans)", fontSize: 11, fill: "var(--muted)" }}>
    <text x="280" y="55" textAnchor="middle">response 1</text>
    <text x="280" y="98" textAnchor="middle">request 2</text>
    <text x="280" y="180" textAnchor="middle">request 3, and every request after</text>
  </g>
</svg>
<figcaption>Set once by the server. Sent back by the browser every time.</figcaption>
</figure>

- **Set-Cookie** = a response header. The server says "please keep this".
- **Cookie** = a request header. The browser says "here is what you gave me".
- **Session cookie** = deleted when the browser closes. It has no expiry date.
- **Persistent cookie** = kept until its `Max-Age` or `Expires` time.
- **First-party cookie** = set by the site you are visiting. This lesson only uses these.

**Simple lesson:** The server sets a cookie once. The browser sends it back every time.

## Setting and Reading Cookies

- **res.cookie(name, value, options)** = adds a `Set-Cookie` header. Built into Express.
- **cookie-parser** = middleware that reads the `Cookie` header into `req.cookies`.
- Install it with `npm install cookie-parser`, then `app.use(cookieParser())`.
- A missing cookie is simply `undefined`.
- Objects are saved as JSON with a `j:` prefix. cookie-parser turns them back into objects.

- The lab's client keeps cookies, like a browser. Press the quick requests in order.
- Open **Headers** after `/theme/dark` to see the `Set-Cookie` line.
- Press **↻ Restart** to forget all cookies, like a new browser.

Easy way to remember: **the server sets, the browser returns.** Your code never has to send cookies back by hand.

**Simple lesson:** `res.cookie` writes, `req.cookies` reads.

## Cookie Options

- **maxAge:** how long to keep it, in milliseconds. Makes it persistent.
- **expires:** an exact end date. Use `maxAge` or `expires`, not both.
- **path:** only send the cookie for URLs under this path. Default `/`.
- **domain:** which domain gets it. Leave it out for "this site only".
- **httpOnly:** browser JavaScript cannot read it. Use it for anything important.
- **secure:** only sent over HTTPS.
- **sameSite:** `"strict"`, `"lax"` or `"none"`. Controls sending from other sites.
- **signed:** adds a signature, so changes can be detected.

**Simple lesson:** `httpOnly`, `secure` and `sameSite` are your three safety switches.

## Signed Cookies

A user can edit their own cookies. A signature shows if they did.

- Give cookie-parser a secret: `cookieParser("my-secret")`.
- Set with `{ signed: true }`. The value becomes `s:value.signature`.
- Read signed cookies from **req.signedCookies**, not `req.cookies`.
- A changed cookie reads as `false`.
- **Signed is not encrypted.** Anyone can still read the value. Never put secrets in it.

- The third request sends a forged cookie. The signature does not match, so it reads `false`.

**Simple lesson:** Signing stops changes. It does not hide the value.

## Clearing Cookies

- **res.clearCookie(name, options)** = tells the browser to delete it.
- Pass the **same** `path` and `domain` you used when setting it.
- Otherwise the browser treats it as a different cookie, and the old one stays.
- Express sends the cookie with an expiry date in the past.

Never store passwords, phone numbers or marks in cookies. Users can read them, and attackers may steal them.

**Simple lesson:** Clear a cookie with the same options you set it with.

## Sessions with express-session

**Session** = server-side data for one visitor, plus a cookie holding only its ID.

- **Session ID** = a long random string. It is the "library card number".
- **Session store** = where the data lives on the server.
- Install it: `npm install express-session`.
- Read and write data on **req.session**, like a normal object.
- Express saves changes at the end of each request.

- The count grows, because the browser sends the same session cookie back.
- The last request sends no cookie, so it starts a new session at 1.
- The cookie only holds the ID. The count lives on the server.

### Session Options

- **secret:** signs the session cookie. Use a long random value from `.env`.
- **name:** the cookie name. Default `connect.sid`. A neutral name hides what you use.
- **resave:** save even if nothing changed. Usually `false`.
- **saveUninitialized:** save empty new sessions. Usually `false`, so visitors get no cookie until needed.
- **rolling:** reset the expiry on every request.
- **cookie:** the cookie options, such as `maxAge` and `secure`.
- **proxy:** trust the proxy's HTTPS header for `secure` cookies. See **Redirects, Content Negotiation and Proxies**.

**Simple lesson:** Data lives in `req.session`. The cookie only carries the ID.

## Session Stores

- The default store is **MemoryStore**: sessions live in the server's memory.
- MemoryStore loses every session when the server restarts.
- It also leaks memory and does not work with more than one server.
- Real apps use an **external store**, such as Redis (`connect-redis`) or a database.
- Pick a store that says it supports express-session, then pass it as `store`.

```js
import { RedisStore } from "connect-redis";
import { createClient } from "redis";

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}));
```

In this lab, MemoryStore is fine, because the lab server only lives a few minutes. Never use it on a real server.

**Simple lesson:** MemoryStore is for learning. Real servers need Redis or a database.

## Logging In and Protecting Routes

A login flow with sessions has five parts:

1. **Check the password** against a stored hash, never plain text.
2. **Regenerate the session** with `req.session.regenerate()`. This prevents session fixation.
3. **Store the user ID** in `req.session.userId`.
4. **Protect routes** with middleware that checks `req.session.userId`.
5. **Log out** with `req.session.destroy()` and `res.clearCookie()`.

### Pages vs APIs

- A **page** route should **redirect** to `/login` when the user is not logged in.
- An **API** route should answer **401** with JSON. A program cannot follow a login page.

- Before login: the API gets 401, and the page redirects.
- A wrong password gets 401 with a vague message. Do not say which part was wrong.
- After logout, the session is gone on the server too.

**Simple lesson:** Hash passwords, regenerate on login, check on every protected route, destroy on logout.

## Session Security

- **Session fixation:** an attacker plants a known session ID before you log in. `regenerate()` gives you a new ID, so the planted one is useless.
- **Secure cookies:** set `cookie.secure: true` in production, so the ID only travels over HTTPS.
- **Absolute expiry:** `maxAge` ends a session after a fixed time.
- **Inactivity timeout:** `rolling: true` with a short `maxAge` ends idle sessions.
- **Strong secret:** a long random value in `.env`, never in code.
- **Errors:** pass store errors from `regenerate`, `save` and `destroy` to `next(err)`.

- The first request is plain HTTP, so **no** `Set-Cookie` header comes back.
- The second pretends a proxy handled HTTPS, so the cookie is sent.

**Simple lesson:** Use secure cookies, and regenerate the session at login.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Reading `req.cookies` without cookie-parser | Express parses cookies | Add `cookieParser()` | `req.cookies` is `undefined` |
| Reading a signed cookie from `req.cookies` | All cookies are in one place | Signed ones are in `req.signedCookies` | The value is missing |
| Storing the user's role in a plain cookie | The browser keeps it safe | Users can edit cookies | Anyone becomes admin |
| No `regenerate()` after login | The old session is fine | Get a new session ID at login | Session fixation attacks |
| MemoryStore in production | It works in development | Use Redis or a database | Logouts on restart, memory leaks |
| Storing plain passwords | Hashing is optional | Store a salted hash only | A leak exposes every password |
| `secure: true` with plain HTTP and no proxy setup | The cookie is still sent | Secure cookies need HTTPS | Logins silently never stick |

</div>

## Check Yourself

## Exercises

## What to Remember

- Cookies travel in `Set-Cookie` (out) and `Cookie` (back in).
- `res.cookie` sets them, cookie-parser reads them, `res.clearCookie` removes them.
- Use `httpOnly`, `secure` and `sameSite`. Signing stops changes but does not hide values.
- Sessions keep data on the server. The cookie only carries the session ID.
- Hash passwords, regenerate on login, protect routes, destroy on logout.
- MemoryStore is for learning only. Use Redis or a database in production.

Next: **File Handling**. You will send files, offer downloads and accept uploads.
