Authentication And Identity Systems
Token Implementation In PHP
The earlier lessons in this track designed authentication flows; this one covers the two concrete mechanisms PHP uses to carry a proven identity across requests — sessions and JWTs — and how to implement each safely. The choice between them is one of the most consequential and most misunderstood decisions in web auth, so start there.
Sessions Versus JWTs
A session stores the state on the server and hands the browser only an opaque session ID in a cookie. Each request, the server looks up the ID to find who the user is. A JWT puts the identity in the token itself — a signed, self-contained claim the server validates without a lookup.
The trade is the mirror image of each other:
- Sessions are revocable instantly (delete the server record) but require server-side storage that every request consults — trivial on one server, needing shared storage (Redis, database) across a fleet.
- JWTs are stateless and scale effortlessly (no lookup) but are not revocable before expiry, because nothing is checked against a server record.
The practical guidance is unfashionable but correct: for a server-rendered web application, sessions are usually the better default. They are simpler, revocable, and PHP has them built in. JWTs earn their place for stateless APIs, cross-service auth, and mobile clients — and even then, the access/refresh split pairs a stateless access JWT with a revocable server-side refresh token precisely to buy back the revocability JWTs lack. Reaching for JWTs to log into a monolith is a common cargo-cult mistake.
Sessions Done Right In PHP
PHP's session handling is solid; the security is in the cookie flags and a couple of disciplines:
<?php
declare(strict_types=1);
// Set before session_start(), typically in php.ini or via session_set_cookie_params.
$cookieParams = [
'httponly' => true, // JavaScript cannot read the cookie (blunts XSS theft)
'secure' => true, // sent only over HTTPS
'samesite' => 'Lax', // not sent on cross-site requests (blunts CSRF)
'path' => '/',
];
echo 'httponly=' . ($cookieParams['httponly'] ? 'yes' : 'no') . PHP_EOL;
echo 'secure=' . ($cookieParams['secure'] ? 'yes' : 'no') . PHP_EOL;
// Prints:
// httponly=yes
// secure=yes
Three flags carry most of the security: HttpOnly keeps the session ID out of JavaScript's reach so an XSS bug cannot exfiltrate it; Secure keeps it off plaintext HTTP; SameSite stops the browser from attaching it to cross-site requests, which is a strong CSRF defense. Two disciplines complete it: regenerate the session ID on privilege change (session_regenerate_id(true) at login) to defeat session fixation, where an attacker plants a known ID before login and inherits the authenticated session; and set an idle timeout and absolute lifetime so abandoned sessions die. Store sessions in Redis or the database once you run more than one server, or users bounce between servers that do not share session state.
JWTs Done Right In PHP
A JWT has three parts — header, payload, signature — base64url-encoded and dot-joined. The signature is the entire security: it proves the token was minted by you and not altered. Use a maintained library (firebase/php-jwt or lcobucci/jwt), never hand-roll parsing, and observe the non-negotiables:
- Verify the signature on every request, and pin the algorithm. The infamous JWT vulnerability is the
alg: none/ algorithm-confusion attack, where an attacker changes the header to claim no signature (or tricks an RS256 verifier into accepting an HS256 token signed with the public key). Always specify the expected algorithm to the verifier rather than trusting the token's own header. - Validate the standard claims:
exp(expiry — reject expired),iss(issuer),aud(audience — the token was minted for you), andnbf/iatas appropriate. - Keep the payload non-secret. A JWT is signed, not encrypted — anyone can base64-decode and read it. Never put passwords, secrets, or sensitive personal data in the claims.
- Protect the signing key like any other secret, and keep access-token lifetimes short because, as established, you cannot revoke a JWT before it expires.
Where Tokens Live In The Browser
For browser clients, prefer an HttpOnly cookie over localStorage for anything long-lived. localStorage is readable by any script, so an XSS flaw becomes token theft; an HttpOnly cookie is not script-readable. If you must hold an access token in the page (common for SPAs calling APIs), keep it in memory only and rely on a refresh flow, so there is no persistent store for an attacker to loot. Cookies bring CSRF into scope, which SameSite plus CSRF tokens handle — the trade is deliberate, not accidental.
What To Check
Before moving on, make sure you can:
- state the session/JWT trade-off and default to sessions for server-rendered apps
- set
HttpOnly,Secure, andSameSiteand say what each defends - regenerate session IDs on login and explain session fixation
- verify JWT signatures with a pinned algorithm and validate
exp/iss/aud - explain why a JWT payload is readable and must hold no secrets
- choose browser storage (HttpOnly cookie or memory) over localStorage
What You Should Be Able To Do
After this lesson, you should be able to implement secure PHP sessions with correct cookie flags and fixation defense, implement JWT validation that resists the algorithm-confusion attacks, choose between sessions and tokens on the merits rather than fashion, and store tokens in the browser without creating an XSS theft primitive.
Practice
Practice: Choose And Implement A Session Mechanism
For two applications — a server-rendered PHP monolith and a stateless JSON API with a mobile client — choose sessions or JWTs for each, justify the choice, and specify the concrete security settings each implementation needs.
Show solution
API with mobile client: JWT access token + revocable refresh token. Statelessness genuinely helps (no session lookup per API call, easy horizontal scale), and the mobile client is not a browser. Implementation: access JWT ~15 min, signed with a pinned algorithm, verified on every request with exp/iss/aud checks; refresh token opaque, stored hashed server-side, rotated on use with reuse-detection (per the access/refresh lesson). For the web frontend of the same API, access token in memory and refresh token in an HttpOnly cookie; for the mobile app, refresh token in platform secure storage.
The two choices illustrate the rule: sessions by default for server-rendered apps, JWTs where statelessness pays and paired with a revocable refresh token to restore control.
Practice: Diagnose Token Implementation Bugs
Task
Identify every security bug and its consequence.
Show solution
- Trusting the token's
algheader. Accepting whatever algorithm the token claims enables the classic attacks:alg: none(attacker strips the signature entirely and the verifier accepts an unsigned token) and algorithm confusion (an RS256 verifier tricked into treating the public key as an HMAC secret). Either yields forged tokens with any claims the attacker wants — total auth bypass. Fix: pin the expected algorithm at verification, never read it from the token. - "Verifying" by decoding without checking the signature. If the code base64-decodes and reads
expbut does not cryptographically verify the signature, the token is unauthenticated input. Combined with the point above, the JWT provides no security at all. Fix: use a library that verifies the signature with the pinned key. - Role in the payload, trusted for authorization. Even with signing fixed, this is fragile: the moment signature verification is weak (as here), the attacker sets
role: admin. And a JWT payload is readable by anyone, so any sensitive claim is disclosed. Authorization should be re-derived server-side or at minimum depend on a properly verified token; sensitive data never goes in claims. - localStorage storage. Readable by any script, so an XSS flaw becomes token theft. Fix:
HttpOnlycookie, or memory-only with a refresh flow. - Admin session cookie with no flags. No
HttpOnly(XSS can steal the admin session), noSecure(sent over plain HTTP), noSameSite(CSRF against admin actions). For the highest-privilege surface in the app, this is the worst place to omit them. Fix: all three flags plus session regeneration on login.
The unifying failure is trusting unverified input: the token's algorithm, the token's claims, and the storage all assume good faith where the whole job is to assume none.
Practice: Define Token Implementation Checks
Show solution
- A test inspects session cookies and asserts
HttpOnly,Secure, andSameSiteare set — on the admin surface especially. - A test asserts the session ID changes across login (
session_regenerate_id), guarding against fixation regressions. - JWT tests feed the verifier an
alg: nonetoken, a token signed with the wrong key, and one with a swapped algorithm, asserting each is rejected — these encode the algorithm-confusion defense directly and are the most important JWT tests to have. - Tests assert
exp,iss, andaudare all validated, and that an expired or wrong-audience token fails. - A test/grep guard asserts no secrets or sensitive personal data appear in JWT claims.
- A check confirms browser clients use
HttpOnlycookies or in-memory storage, neverlocalStorage, for long-lived tokens.
Human, periodically:
- Confirm the session-versus-JWT choice still fits each surface — a monolith that grew a JWT login for no reason, or an API still doing per-request session lookups at scale, are both worth revisiting.
- Verify signing keys are stored as secrets and rotated, and that access-token lifetimes remain short given JWTs' non-revocability.
- Confirm session storage is shared (Redis/DB) wherever more than one server runs, so revocation and lookup are consistent across the fleet.