What you'll learn: By the end of this lesson you will be able to say precisely what authentication and authorization each decide, trace a login into a server-side session and explain each cookie attribute that protects it, compare sessions with signed and opaque tokens by what each can and cannot revoke, place the authorization check at the object level on the server, and choose a credential type for a browser app, a mobile app and a service call.
A hotel front desk does two separate jobs. First the clerk checks your passport and finds your booking: that is authentication, proving who you are. Then the clerk hands you a key card that opens room 412 and the gym but not room 413: that is authorization, deciding what you may do. Afterwards every door checks the card, not your face. The card carries the proof of the first job into the second.
Web systems copy this design: a login proves identity once, a session or a token carries the proof, and every request is authorized again. Most breaches here come from mixing the jobs up, a perfect login in front of an endpoint that never asks whether this user may touch this record.
Security for Distributed Systems gives the threat-modelling frame and an OAuth overview, and ShopStream Stage 10 picks a credential per client. This lesson goes underneath both: how a session works on the wire, what a token changes, and where the permission check must live. OAuth and OpenID Connect are separate topics.
The core idea
Authentication establishes an identity once; a credential carries it forward; authorization is decided again, on the server, for every request and every object.
Three illustrations run through the lesson:
- Minimal example: a server-side session in about fifteen lines of JavaScript.
- Realistic example: ShopStream's store dashboard and mobile app, in "In practice".
- Failure example: a signed-in user reading someone else's order by changing an id, and session fixation.
Authentication: proving who you are
Authentication checks a claim ("I am shopper 42") against evidence of a kind called a factor: something you know, something you have, something you are. NIST SP 800-63B (Revision 4 at the time of writing) defines three authenticator assurance levels. Its overall reauthentication limits are no more than 30 days at AAL1, 24 hours at AAL2 (inactivity 1 hour) and 12 hours at AAL3 (inactivity 15 minutes): useful reference points when someone asks how long a login should last.
Its password rules contradict old habits. A password used alone should be at least 15 characters (8 inside multi-factor use), and a verifier should accept at least 64. Verifiers must not impose composition rules or periodic changes, must check passwords against common and compromised lists, and must store them salted and hashed with a suitable scheme.
The OWASP Authentication Cheat Sheet adds operations: return the same generic message and status whether the account is missing, the password wrong or the account locked, so the form does not reveal which accounts exist; count failed logins per account, not only per address; and ask for the current credential again before sensitive changes such as a new email address.
Figure 1. A login that ends in a session.
How to read this: the password travels once. After that the cookie is the only proof, so protecting the cookie is protecting the account.
Authorization: deciding what you may do
Authorization asks: may this identity perform this action on this resource now? The OWASP Authorization Cheat Sheet gives the backend rules:
- Deny by default. Access exists only where a rule grants it.
- Check every request, with global checks rather than one-off checks in individual methods.
- Enforce on the server. Client-side checks improve the interface but are never the control.
- Check the object, not only the user. Guessable ids enable insecure direct object reference (IDOR); hiding ids is not a defence by itself. The OWASP API list calls it broken object-level authorization.
- Fail safely and test the logic, including tests that prove access is denied by default.
Figure 2. Three questions on every request.
(authentication)"} Q1 -->|"no"| E1["401: stop"] Q1 -->|"yes"| Q2{"May this role or scope
do this action?"} Q2 -->|"no"| E2["403: stop"] Q2 -->|"yes"| Q3{"Does this user own or share
this specific object?"} Q3 -->|"no"| E3["403 or 404: stop"] Q3 -->|"yes"| OK["Serve the request"]
How to read this: the third diamond is the one teams forget. The first two can be written once in a gateway or a filter; the third needs the object, so it lives next to the data.
Authenticate at the edge if you like, but authorize in the service that owns the data: a gateway can say who the caller is, only the service holding order 1234 can say whether the caller may read it. See the API gateway lesson for the edge.
Sessions: the server remembers
A session is server-side state. After login the server creates a record, gives the browser an opaque identifier (a random string that means nothing by itself) and looks the record up on every request. The identifier travels in a cookie.
RFC 6265 defines the mechanism: the server sends Set-Cookie, the browser returns Cookie on matching requests. The RFC calls cookies ambient authority: the browser attaches them automatically, which is what makes cross-site request forgery possible. The attributes are your controls.
| Attribute | What it does | Limit |
|---|---|---|
Secure |
Sends the cookie only over secure channels such as TLS | Confidentiality only, not integrity against an active attacker (RFC 6265, 4.1.2.5) |
HttpOnly |
Hides the cookie from script APIs such as document.cookie |
The browser still sends it with script-initiated requests (MDN) |
Domain |
Widens the cookie to a host and its subdomains; omitted, it goes to the origin host only | A wide value exposes it to sibling subdomains (OWASP) |
Path |
Limits which paths receive it | RFC: "cannot be relied upon for security" |
Max-Age, Expires |
Set a lifetime; Max-Age wins |
Client-side expiry is not enforcement (NIST) |
SameSite |
Strict, Lax or None: sent on cross-site requests or not |
Not in RFC 6265, documented by MDN; defence in depth, not a replacement for CSRF tokens (OWASP) |
The __Host- prefix (MDN) makes the browser require Secure, HTTPS, no Domain and Path=/. OWASP recommends it for session cookies; browsers without prefix support accept such cookies anyway, so it is an extra control.
Illustration 1 (minimal). This creates the record, builds the header and shows that an attacker-chosen id is refused. It was run with Node on 2026-11-08.
import { randomBytes } from "node:crypto";
const sessions = new Map(); // sessionId -> { userId, createdAt, lastSeen }
function login(userId, now) {
const id = randomBytes(32).toString("base64url"); // 256 bits from the OS CSPRNG
sessions.set(id, { userId, createdAt: now, lastSeen: now });
return `Set-Cookie: __Host-sid=${id}; Secure; HttpOnly; SameSite=Lax; Path=/`;
}
const header = login("shopper-42", 0);
const id = header.split("=")[1].split(";")[0];
console.log(header.replace(id, "<id>"));
console.log("id length:", id.length, "characters");
console.log("known id accepted:", sessions.has(id));
console.log("unknown id accepted:", sessions.has("attacker-chosen-value"));
sessions.delete(id); // logout: server-side invalidation
console.log("after logout:", sessions.has(id));
Output:
Set-Cookie: __Host-sid=<id>; Secure; HttpOnly; SameSite=Lax; Path=/
id length: 43 characters
known id accepted: true
unknown id accepted: false
after logout: false
OWASP sets 64 bits of entropy from a secure generator as the floor; this id has 256 and holds no user data, as OWASP and NIST require of an opaque cookie. A real store adds timeouts.
The session rules that matter, from the OWASP Session Management Cheat Sheet:
- Regenerate the id after any privilege change, login above all, and destroy the old one. Never accept an id the server did not generate.
- Enforce an idle and an absolute timeout on the server. The sheet's examples: 2 to 5 minutes idle for high-value applications, 15 to 30 for low-risk ones, 4 to 8 hours absolute for an office-day application.
- Invalidate on the server at logout. Deleting the browser cookie does not invalidate a stolen copy.
- Keep ids out of URLs and out of
localStorageandsessionStorage, where any script on the origin can read them.
Tokens: the credential carries its own proof
A token is a credential presented without a matching session record on the server. An opaque token is a random string like a session id, sent in an Authorization header and checked by asking an identity service what it means: lookup per request, instant revocation. A signed token carries claims and a signature. The common format, the JSON Web Token (RFC 7519), is "a compact, URL-safe means of representing claims". Any service with the issuer's public key verifies it with no call at all.
RFC 7519 defines optional registered claims: iss (issuer), sub (subject), aud (audience), exp (expiry, "on or after which the JWT MUST NOT be accepted"), nbf, iat and jti. Most JWTs are signed only, so anyone holding one can read the claims; the signature proves they were not changed.
Figure 3. Where the proof lives.
Figure 3. How to read this: the top path asks a store on every request, which makes revocation instant and adds a dependency. The bottom path asks nobody, which removes the dependency and removes the off switch.
Because verification is local, nothing tells the API a token was revoked: it stays valid until exp. Three consequences follow. Keep the access token short and pair it with a longer-lived refresh token the identity service can revoke. A denylist of revoked token ids works but reintroduces the shared store you avoided. And a role baked into a token is stale until expiry: if a store employee is removed at noon, a token minted at 11:55 still says they work there.
Signed tokens also push verification onto every service. RFC 8725, the JWT best-current-practice document, says to verify the algorithm, validate every cryptographic operation and validate the audience: a token whose aud is missing or does not name the recipient must be rejected. It also warns that the none algorithm (an unsigned JWT) belongs only where other means protect the token. OWASP's API security list (API2:2023 Broken Authentication) names the same mistakes in the field: accepting unsigned or weakly signed JWTs and not checking expiry.
Choosing: sessions, opaque tokens or signed tokens
| Property | Server-side session | Opaque token | Signed token (JWT) |
|---|---|---|---|
| Where the truth lives | Session store | Identity service | In the token |
| Revocation | Immediate | Immediate | At expiry, or via a denylist |
| Cost per request | One store lookup | One introspection call (cacheable, with staleness) | Signature check in memory |
| Works well for | Browser app with one backend | APIs that must revoke fast | Many services verifying independently |
| Main risk | Store outage; CSRF | Dependency on the identity service | Long-lived or over-trusted tokens; verification bugs |
| Stale permissions | No | No | Yes, until expiry |
Figure 4. A decision, not a loyalty.
backend of its own?"} Q1 -->|"yes"| C1["Server-side session
HttpOnly Secure cookie"] Q1 -->|"no"| Q2{"Must revoke within seconds?"} Q2 -->|"yes"| C2["Opaque token
with introspection"] Q2 -->|"no"| Q3{"Many services verify
without a shared store?"} Q3 -->|"yes"| C3["Short-lived signed token
+ revocable refresh token"] Q3 -->|"no"| C1
How to read this: the cookie session is the default for a web app because the browser already handles it. Move to tokens when the client is not a browser or many verifiers make a shared store awkward.
The other side of this topic. The browser decides which buttons to show, holds the cookie or token, and must cope with a 401 in the middle of a form. The Frontend System Design track plans a lesson on authentication and authorization in the interface (not published yet, so no link). The back-end rule: the interface may hide a button, but the server never relies on that. For cookies across origins see CORS, preflight and credentials.
In practice: one marketplace, three kinds of client
This is the realistic example, on ShopStream, the course's running system. It is an educational scenario and every number is an illustrative assumption.
This is a realistic scenario, not a case history.
The situation
ShopStream has three callers: shoppers on a web app and a mobile app, store staff on a dashboard, and internal services such as Payments. The course's stage-end assumption is about 2,300 browsing requests per second at peak (illustrative). When Stage 2 puts several app servers behind a load balancer, sessions kept in one server's memory break: a shopper logged in on server A is a stranger on server B.
Starting architecture
Figure 5. Three clients, three credentials.
How to read this: the gateway answers "who is calling". The Orders service, which owns the data, answers "may they see this order", using the owner or store id in the query.
Options considered
| Option | Gain | Cost | Wrong when |
|---|---|---|---|
A. Signed tokens everywhere, in localStorage |
One mechanism | One script flaw exposes every token; staff removed at noon stay valid | A browser holds high-value access |
| B. Session cookie for the dashboard, short-lived tokens plus refresh for mobile | Instant staff revocation; nothing in browser storage | Two mechanisms, a shared store | The team cannot operate a store |
| C. Opaque tokens everywhere | Instant revocation everywhere | A call on the hot path | The latency budget is tight |
The decision
Option B. Dashboard sessions live in a shared store, are regenerated at login and carry idle and absolute timeouts (illustrative: 30 minutes and 8 hours, from OWASP's examples). Mobile apps hold a short-lived signed token and a refresh token the identity service can revoke. Every service, whatever the credential, receives an identity and calls an authorization function for the object.
Pseudocode: order read
caller = identity from session or token // already authenticated
order = orders.find(id)
if order is missing -> 404
if caller is not order.shopper
and caller is not staff of order.store -> 404 // do not reveal it exists
return order
Rollout and verification
Roll out the session store, then the dashboard, then mobile. For each endpoint run a two-user test: user A creates a record, user B requests it by id and must be refused; staff of store 1 requests an order of store 2 and must be refused. Add a test that an endpoint with no rule returns an error (deny by default). Watch session-store latency and 403 counts per endpoint.
The 2 a.m. failure
Illustration 3 (failure). A store manager reports seeing another store's orders. The dashboard endpoint checked the role store_staff but not that the order belonged to the caller's store: authenticated, role-correct and still wrong. Check by querying for orders served to a caller whose store id differs from the order's. Mitigate by blocking the endpoint and deploying the owner check. The lasting fix is one shared object check used by every handler, the two-user test on every new endpoint, and an alert on a rise in not-found responses, which is what id guessing looks like.
Session fixation is a second failure of the same family. An attacker plants a known session id in the victim's browser before login; if the server keeps it afterwards, the attacker shares the authenticated session. OWASP's prevention: accept only ids the server generated, and regenerate at login, as the minimal example does.
Architect's notes
- Decide the revocation story first. "How fast must we cut off a stolen credential or a dismissed employee?" picks the type better than fashion does.
- Make the object check unforgettable: one shared call and one test pattern.
- Keep the browser credential out of script reach. An
HttpOnlycookie, or a backend holding tokens for the browser, beats web storage. - Validate issuer, audience and expiry in every service that accepts a token.
- Review against a checklist. The OWASP Application Security Verification Standard (stable version 5.0.0, published 30 May 2025, at the time of writing) lists testable requirements for these controls.
Common mistakes
- Treating a valid login as permission.
- Authorizing only in the client or the gateway.
- Long-lived signed tokens with no revocation path.
- Session ids or tokens in URLs or
localStorage. - Keeping the same session id across login.
- Different errors for "no such user" and "wrong password".
- Calendar password rotation and composition rules. Current NIST guidance says not to.
Key takeaways
- Authentication proves who you are; authorization decides what you may do, on every request.
- A session is server-side state behind an opaque random id in a cookie;
Secure,HttpOnly,SameSite, a narrowDomainand the__Host-prefix each close a specific hole. - Regenerate the id at login, enforce idle and absolute timeouts on the server, invalidate on the server at logout.
- A signed token verifies locally and cannot be recalled before
exp: keep it short-lived with a revocable refresh token. - Validate algorithm, issuer, audience and expiry; never accept unsigned tokens.
- The commonest authorization bug is the missing object-level check: deny by default, check ownership beside the data.
- Pick the credential by revocation need and client type.
Practice exercises
Exercise 1 (30 min). Run the minimal example. Add a 30-minute (illustrative) idle timeout with an injected clock and a test that a session unused for 31 minutes is refused.
Exercise 2 (45 min). Write the three checks of Figure 2 as pseudocode for ShopStream's refund endpoint, then list the two-user tests that would catch a missing object check.
Exercise 3 (45 min). A store employee is dismissed at noon. For a session, an opaque token and a signed token with a 15-minute (illustrative) lifetime, state when access really ends and how you would end it sooner.
Exercise 4 (1 hour). Write the Set-Cookie header for a session cookie that must work in an embedded cross-site widget. State which attributes change, what you give up and which other defence you add.
Related reading
- Security for Distributed Systems: the threat-model frame and the OAuth overview.
- ShopStream Stage 10: Secure and Operate: credentials per client in context.
- API Gateways and Service Meshes: where the edge checks sit.
- Rate Limiting: the brute-force defence for login endpoints.
- Multi-Tenancy: object checks when many stores share one system.
- CORS, Preflight and Credentials: cookies across origins, from the browser's side.
Official documentation
- IETF, RFC 6265: HTTP State Management Mechanism: cookies, attribute semantics, security considerations.
- NIST, SP 800-63B: Authentication and Authenticator Management: assurance levels, password rules, session management.
- OWASP Cheat Sheet Series: Session Management, Authorization and Authentication.
- IETF, RFC 7519: JSON Web Token and RFC 8725: JWT Best Current Practices.
- OWASP, Application Security Verification Standard (version 5.0.0).
- MDN, Set-Cookie header:
SameSite, prefixes,HttpOnly.