Networking · Lesson 8 of 12 · 25 min

Sessions, JWT, CORS, Load Balancers and Proxies

Where identity lives (cookies, sessions, JWT), what CORS really restricts, and how L4 and L7 balancers and proxies sit in front of servers.

✓ Choose between cookie sessions and JWTs and say what revoking costs✓ Explain preflight and why CORS protects browsers, not servers✓ Tell L4 from L7 balancing and a forward proxy from a reverse proxy
Say it in 30 seconds
  • 3 partsJWT: header, payload, signature
  • ~15 minTypical short access-token lifetime
  • L4 / L7Connection routing versus request routing
Show what to say

“A session keeps state on the server behind an HttpOnly, Secure, SameSite cookie, so it can be revoked instantly. A JWT is signed and stateless, so any service can verify it, but it lives until it expires. CORS is the browser asking the server's permission for cross-origin reads, with a preflight for non-simple requests. L4 balances connections, L7 reads HTTP and can route and terminate TLS.”

Trade-offs

  • Sessions vs JWT: sessions revoke at once but need a lookup per request; a JWT needs no lookup but cannot be revoked before it expires.
  • L4 vs L7 balancer: L4 is fast and protocol-agnostic; L7 routes by path or header and terminates TLS but uses more CPU.
  • Sticky sessions vs shared store: stickiness is easy but loads unevenly and loses sessions on failure; a shared store keeps servers stateless.

Bring it up before they ask

CORS is enforced by browsers only, so say it protects users and that the API still needs its own authentication.

0 of 0 sections ticked
30-second answer

HTTP is , so identity travels in a cookie (an ID for a server-side session) or a signed . is the browser deciding whether a page may read a cross-origin reply, and balancers and proxies route by IP and port (L4) or by what the request says (L7).

Cookies and sessions

The server cannot remember you between requests, so it hands the browser a cookie to send back every time.

Read more · 124 words

In a session, the cookie is only a random ID and the data stays on the server, often in .

AttributeWhat it does
HttpOnlyJavaScript cannot read the cookie, so an XSS bug cannot steal it
SecureSent only over HTTPS
SameSite=LaxNot sent on cross-site requests, which blocks most CSRF
Max-AgeWhen the browser drops it

Press Play to watch it run · 7 steps

The server checks the password, stores the session on its side and hands the browser only an opaque ID in a cookie. Every later request carries the cookie and the server looks the session up.

A cookie session: log in once, then show the cookieThe server checks the password, stores the session on its side and hands the browser only an opaque ID in a cookie. Every later request carries the cookie and the server looks the session up.POST /login (email, password)SET sess:9f3a = user 42, TTL 30 minSet-Cookie: sid=9f3a; HttpOnly; Secure; SameSite=LaxThe browser now attaches the cookie to every request to this siteGET /account Cookie: sid=9f3aGET sess:9f3auser 42200 OK (account page)
  • Client
  • Service
  • Cache
Text description
  1. Browser → App server: POST /login (email, password)
  2. App server → Session store: SET sess:9f3a = user 42, TTL 30 min
  3. App server replies to Browser: Set-Cookie: sid=9f3a; HttpOnly; Secure; SameSite=Lax
  4. Browser → App server: GET /account Cookie: sid=9f3a
  5. App server → Session store: GET sess:9f3a
  6. Session store replies to App server: user 42
  7. App server replies to Browser: 200 OK (account page)

JWT versus server sessions

A is a self-contained token: header, payload of claims (who, until when) and signature.

Read more · 65 words

Any server with the key can verify it with no lookup, which suits many services sharing one login.

The payload is signed, not encrypted, so keep secrets out. The cost is revocation: until exp passes the token is valid, so banning someone needs a deny-list or a lifetime of minutes plus a refresh token. A session is the opposite: one lookup per request, ended instantly.

import { createHmac, timingSafeEqual } from 'node:crypto';

// A JWT is three base64url parts: header.payload.signature. The payload is only SIGNED, not encrypted:
// anyone can read it, but nobody can change it without the secret.
const encode = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const decode = (part) => JSON.parse(Buffer.from(part, 'base64url').toString());
const sign = (data, secret) => createHmac('sha256', secret).update(data).digest('base64url');

export function issue(claims, secret, { ttlSeconds = 900, now = Date.now() } = {}) {
  const iat = Math.floor(now / 1000);
  const header = encode({ alg: 'HS256', typ: 'JWT' });
  const payload = encode({ ...claims, iat, exp: iat + ttlSeconds });
  return `${header}.${payload}.${sign(`${header}.${payload}`, secret)}`;
}

export function verify(token, secret, { now = Date.now() } = {}) {
  const [header, payload, signature, extra] = token.split('.');
  if (!header || !payload || !signature || extra !== undefined) throw new Error('malformed');
  if (decode(header).alg !== 'HS256') throw new Error('unexpected algorithm'); // never let the token choose its own check
  const expected = Buffer.from(sign(`${header}.${payload}`, secret));
  const given = Buffer.from(signature);
  if (expected.length !== given.length || !timingSafeEqual(expected, given)) throw new Error('bad signature');
  const claims = decode(payload);
  if (claims.exp * 1000 <= now) throw new Error('expired');
  return claims;
}

CORS

Browsers apply the same-origin policy: a script cannot read replies from another origin (scheme, host, port).

Read more · 91 words

is how a server opts in, through Access-Control-Allow-* headers. For a PUT, a JSON body or an Authorization header, the browser first sends an OPTIONS preflight and continues only if allowed.

CORS is enforced by the browser only. curl and other servers ignore it, so it protects users, not your API. With cookies the server must echo a specific origin and send Allow-Credentials; * is not allowed.

Press Play to watch it run · 2 steps

A simple GET goes straight out and the browser checks the reply headers. A request with a JSON body and an Authorization header is preceded by an OPTIONS preflight asking whether the server allows it.

CORS: when the browser asks firstA simple GET goes straight out and the browser checks the reply headers. A request with a JSON body and an Authorization header is preceded by an OPTIONS preflight asking whether the server allows it.alt [Simple GET]GET /items Origin: app.example.com200 + Access-Control-Allow-Origin: app.example.comThe browser compares the header with the page's origin and lets the script read the reply
  • Client
  • Service
Text description
  1. app.example.com → api.example.com: GET /items Origin: app.example.com
  2. api.example.com replies to app.example.com: 200 + Access-Control-Allow-Origin: app.example.com
// CORS is the SERVER telling the BROWSER which other sites' scripts may read its responses.
// A preflight is an OPTIONS request the browser sends first for anything beyond a plain GET/POST form.
export function cors(request, config) {
  const { method, origin, requestMethod, requestHeaders = [] } = request;
  const { allowed, methods = ['GET', 'POST'], headers = ['content-type'], credentials = false, maxAge = 600 } = config;
  const preflight = method === 'OPTIONS' && requestMethod !== undefined;

  if (!origin || !allowed.includes(origin)) return { status: preflight ? 403 : 200, headers: {} }; // no CORS headers: the browser hides the reply

  // The origin is echoed back, never '*', because credentials are not allowed with a wildcard.
  const base = { 'access-control-allow-origin': origin, vary: 'Origin' };
  if (credentials) base['access-control-allow-credentials'] = 'true';
  if (!preflight) return { status: 200, headers: base };

  const okMethod = methods.includes(requestMethod);
  const okHeaders = requestHeaders.every((h) => headers.includes(h.toLowerCase()));
  if (!okMethod || !okHeaders) return { status: 403, headers: {} };
  return { status: 204, headers: { ...base, 'access-control-allow-methods': methods.join(', '), 'access-control-allow-headers': headers.join(', '), 'access-control-max-age': String(maxAge) } };
}

Load balancing: L4 versus L7

An L4 balancer picks a backend from IP addresses and ports and forwards packets without reading them: fast and protocol-agnostic.

Read more · 58 words

An L7 balancer terminates the connection and reads the HTTP request, so it can route by path or header, retry, set cookies and terminate , at the cost of CPU.

Algorithms and health checks are in the HLD path.

Interactive

Load balancer simulator

Requests take 0.3–0.8 s. Make server C slow, then compare round robin with least-connections and power-of-two-choices.

0Server Aserved 00Server Bserved 00Server Cserved 00Server Dserved 0

p95 latency: 0.00 s · in flight: 0 · dropped (no healthy server): 0

Proxies: forward and reverse

A forward proxy acts for clients, such as a corporate gateway filtering outbound traffic.

Read more · 203 words

A acts for servers: clients talk to it as if it were the site, and it terminates , , and routes.

nginx, Envoy and HAProxy are reverse proxies; a is one spread across the world; an adds auth and quotas. The proxy passes the real client address in X-Forwarded-For; trust it only when your own proxy set it.

Press Play to watch it run · 6 steps

Clients talk only to the proxy. It terminates TLS, then routes by path: /api goes to the API pool and everything else to the web pool, adding the client's address in a header.

A reverse proxy in front of the serversClients talk only to the proxy. It terminates TLS, then routes by path: /api goes to the API pool and everything else to the web pool, adding the client's address in a header.BrowserReverse proxyTLS ends hereAPI poolx3Web poolx2

Tap any box to see what it is and what it talks to.

  • Client
  • Load balancer
  • Service
Open in playground
Text description
  1. GET https://example.com/api/orders arrives over TLS
  2. The proxy decrypts it, reads the path and forwards to the API pool with X-Forwarded-For: client IP.
  3. The API pool answers the proxy.
  4. The proxy sends the reply back over the original connection.
  5. GET /index.html arrives next.
  6. Different path, different pool: forwarded to the web servers.

Sticky sessions and where state lives

pin a user to one backend (by cookie or IP hash) so in-memory state keeps working.

Read more · 117 words

The costs: load spreads unevenly, and when that server dies the sessions on it die too.

The cleaner design is servers with sessions in a shared store such as , or in a . Then any server can answer any request, and a deploy or a crash costs nothing.

TLS termination and trust boundaries

Terminating at the proxy keeps certificates in one place and offloads the crypto, but traffic behind it is plain unless you re-encrypt it.

Read more · 38 words

Inside a shared network use TLS or mutual TLS (mTLS) between services so a compromised host cannot read the rest. The proxy tells the app which scheme was used (X-Forwarded-Proto) and adds Strict-Transport-Security so browsers insist on HTTPS.

Auth mistakes to avoid

Read more · 144 words
  • Trusting the token's alg. Always verify with the algorithm you chose, never the one the token names.
  • Trusting forwarded headers from the open internet instead of only from your proxy.
  • is delegation, not login. OAuth 2.0 lets an app act for you at another service; OpenID Connect adds identity on top.

Follow-up questions

Sessions or JWT?

Sessions keep state on the server: easy to revoke, one lookup per request. are and verified by any service holding the key, but hard to revoke before they expire, so keep them short-lived.

What is CORS and what is a preflight?

Browsers block scripts from reading cross-origin replies unless the server opts in with Access-Control headers. For non-simple requests the browser first sends an OPTIONS preflight to check the method and headers are allowed.

What is the difference between L4 and L7 load balancing?

L4 forwards connections by IP and port without reading the payload, so it is fast. L7 reads the HTTP request, so it can route by path or header, terminate and retry, using more CPU.

Forward proxy versus reverse proxy?

A forward proxy represents clients to the internet. A represents servers to clients, handling TLS, , routing and in front of them.

Check yourself

Question 1 of 7

Score 2 out of 3 or better to mark this page complete.

1. Which cookie attribute stops JavaScript on the page from reading the cookie?

Next: That covers the network. Next, the database: what an index really costs, and B-trees against LSM-trees.