Skip to main content
Back to module
Back-End Web APIs · Auth and Security

Sessions, cookies, and tokens

Reading13mLesson 1 of 3 in this module

Compare server sessions and tokens, and store them safely in cookies or secure storage.

Why it matters

HTTP is stateless: each request arrives on its own, with no memory of the previous one. Yet users expect to log in once and stay logged in. Every API that has accounts needs a way to attach "this is Ada" to later requests. The two main approaches, server sessions and tokens, each have trade-offs, and picking the wrong one, or storing it in the wrong place, causes real security bugs.

Authentication and authorization

Two words that are easy to mix up:

  • Authentication (authn): who are you? Proving identity with a password, a Google sign-in, a code from an authenticator app.
  • Authorization (authz): what may you do? Ada can edit her own profile but not someone else's; a moderator can hide posts.

This lesson is about carrying the result of authentication across requests. Authorization checks happen on every request after that.

Cookies

A cookie is a small value the server asks the browser to store and send back automatically on every request to the same site:

HTTP/1.1 200 OK
Set-Cookie: sid=8f2c...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800

From then on the browser adds Cookie: sid=8f2c... to each request. The attributes matter:

  • HttpOnly: JavaScript on the page cannot read the cookie, so a cross-site scripting (XSS) bug cannot steal it.
  • Secure: sent only over HTTPS.
  • SameSite=Lax (or Strict): the browser does not send it on most cross-site requests, which blocks many cross-site request forgery (CSRF) attacks.
  • Max-Age / Expires: how long it lasts. Without one, it is deleted when the browser closes.

Approach 1: server sessions

  1. The user logs in with a correct password.
  2. The server creates a random, unguessable session ID, stores { sessionId -> userId, expiresAt } in a database or cache, and sends the ID in an HttpOnly cookie.
  3. On each request, the server looks up the ID and loads the user.
  4. Logging out deletes the session row. The cookie is now worthless.

Strengths: the server can revoke any session instantly ("log out of all devices"), and the cookie holds no data at all. Cost: a lookup per request, and a shared store if you run more than one server.

Approach 2: tokens (JWT)

A JSON Web Token is a string of three base64url parts: header.payload.signature. The payload holds claims:

{ "sub": "user_42", "role": "student", "iat": 1792771200, "exp": 1792774800 }

The server signs it with a secret key. On later requests it verifies the signature and trusts the claims without a database lookup. Important details:

  • A JWT is signed, not encrypted. Anyone can decode the payload. Never put secrets or personal data in it.
  • Always verify the signature and the expiry with a maintained library, and fix the allowed algorithm. Never accept alg: none.
  • A JWT is valid until it expires. Revoking one early needs extra work: short lifetimes plus refresh tokens, a deny list, or a per-user version number stored in the database and checked on each request (which brings back a lookup).

Where the token lives

Tokens can travel in two ways:

HttpOnly cookieAuthorization: Bearer <token> header
Sent automaticallyYes, by the browserNo, your code adds it
Readable by page scriptsNoYes, if stored in localStorage
Main riskCSRF (mitigate with SameSite, CSRF tokens)XSS can steal it
Good fitBrowser apps on your own domainMobile apps, CLIs, server-to-server

For browser apps, a session ID or token in an HttpOnly, Secure, SameSite cookie is the safer default. Mobile apps have no cookie jar by default, so they usually send a bearer token and keep it in the platform's secure storage (Keychain on iOS, Keystore on Android). API keys for server-to-server calls are another kind of bearer token.

Worked example: the request flow

POST /login {email, password}
  -> check password
  -> create session (or sign token)
  -> 200 + Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax

GET /me   (browser sends Cookie: sid=...)
  -> middleware loads the session -> req.user = { id: 42 }
  -> 200 { "id": 42, "name": "Ada" }

GET /me   (no cookie, or expired)
  -> 401

Common mistakes

  • Storing tokens in localStorage in a browser app, where any injected script can read them.
  • Cookies without HttpOnly, Secure and SameSite.
  • Long-lived JWTs with no way to revoke them after a password change or account compromise.
  • Personal data or secrets in a JWT payload.
  • Checking authentication but not authorization: a logged-in user can still try to read someone else's data.
  • Predictable session IDs. Use a cryptographically secure random generator, such as crypto.randomBytes(32).

Quick recap

  • Authentication proves who someone is; authorization decides what they may do.
  • Sessions keep state on the server and are easy to revoke; JWTs carry signed claims and are hard to revoke early.
  • Browser apps: prefer HttpOnly, Secure, SameSite cookies. Mobile and server clients: bearer tokens in secure storage.
  • Whatever you choose, check authorization on every request.

The Do tab quiz asks you to choose the right approach for a few real situations.

Also in this lesson: