JWT Lifecycle Simulator: a token across its short life.
A JWT is a signed, base64url-encoded token that carries its own claims, so a server can verify it without a session lookup. It has three parts (header, payload, signature) and one hard problem: you can issue, verify, expire, and rotate it, but you can't cleanly revoke it before it expires. Step through each phase below.
{
"alg": "RS256",
"typ": "JWT",
"kid": "k-2025-04"
}{
"iss": "https://auth.toolkit.app",
"sub": "user_8120",
"aud": "api.toolkit.app",
"iat": 1781501672,
"exp": 1781502272,
"scope": "read:orders write:orders"
}1cE7…aY9j (RSA-2048 over base64url(header).base64url(payload))
The token is laid out as its three real segments: a header naming the algorithm and key id, a payload of claims including the issuer, subject, audience, and the iat and exp timestamps, and the signature that binds the first two. The buttons drive the token through its life: Verify checks it, the +60s and +10min buttons push the clock forward, Revoke blacklists it, and Rotate mints a fresh one. The phase and time-left readouts up top track its state, and the log records each action.
Verify first and the token passes: the check is pure local work, no database touched. Then push the clock past the ten-minute expiry and verify again, and it flips to DENY without anyone changing the token. That is expiry doing the revoking for you. Now try Revoke, then Verify, and notice the token still looks perfectly valid yet gets denied. What should surprise you is that this denial needs a blacklist the verifier has to consult, which is exactly the state JWTs were supposed to avoid. Revocation is the price of stateless verification.
What is a JWT and what does its lifecycle look like?
Why every API call can't ask the database who you are.
A JSON Web Token (JWT) is a self-describing security token: a header, a payload (claims), and a signature, base64url-encoded and dot-separated. RFC 7519 standardised it in 2015 as part of the JOSE family. JWTs let an API verify a request's identity and authorisation without a database lookup — at the cost of revocation complexity and a long history of implementation bugs.
Imagine you have just signed in to a website. The login form posts your username and password to the server; the server checks the password against the database; everything is fine; you are now “logged in.” But what does that actually mean? HTTP is stateless. Each request is independent. The next time your browser asks for a page, the server has no memory of you whatsoever. Some piece of evidence has to ride along on every subsequent request to prove that you are still the same person who logged in two minutes ago.
The classic answer is a session cookie. The server picks a random 30-byte string — sess_8f2c7e... — writes a row in a database table mapping that string to your user ID, and sets it as a cookie in the browser. Every subsequent request carries the cookie; the server looks the string up in the table; if it finds the row, it knows who you are. Simple, works, has been the dominant pattern since the late 1990s. But it has one cost: every API call performs a database lookup just to identify the caller. For a single monolith with one database that is fine. For a fleet of fifty microservices each handling a thousand requests per second, fifty thousand session-lookup queries per second hammers a single point of contention.
The JSON Web Token, JWT for short, is the alternative that lets the server answer “who is this caller” without any database lookup at all. The trick is to put the answer in the cookie itself, signed by the auth server with cryptographic ink that no one else can forge. A JWT is three pieces — a header saying which signature algorithm was used, a payload of claims like “the user is user_8120, the token expires at 2026-05-02T15:00Z”, and a signature proving the payload was issued by the trusted auth server. To verify, a service hashes the first two parts and checks the hash against the signature using the issuer's public key. About 80 microseconds of CPU. No network round trip, no database, no shared state.
That self-describing property is what made JWTs the default for cross-service authentication in the 2010s. Auth0, Okta, AWS Cognito, Keycloak, Azure Entra, Google Identity, and GitHub Actions all emit JWTs. The catch — and there is always a catch — is that revocation gets harder when the server has no list of valid sessions to delete from. The simulator above lets you walk a token through its short life: issued, verified, expired, revoked, rotated. The next sections trace where the format came from, the famously embarrassing bugs that kept catching the early implementations, and the validation checklist a production verifier has to pass.
JWT structure — header, payload, signature
Header, payload, signature.
A JWT looks like one long opaque string. It is not. Look closely and you will see two periods inside it; the periods split the string into three independent base64url-encoded segments. Decode each segment and you can read the contents in plain text. The format is deliberately transparent: anyone holding the token can see the payload. What they cannot do without the issuer's private key is forge a different one.
The first segment is the header. It is a tiny JSON object with two or three keys: which signature algorithm was used (RS256, ES256, EdDSA), and the key identifier (kid) that tells the verifier which of several rotating public keys to use. The header is metadata about the token; nothing private is supposed to be there.
The second segment is the payload, also called the claims. This is where the interesting content lives. Standard claims are short three-letter codes the spec defines: iss for the issuer URL, sub for the subject (the user ID), aud for the audience (which API the token is meant for), exp for the expiration timestamp, iat for issued-at, and nbf for not-before. Custom claims like scopes, roles, and tenant IDs ride alongside. The payload is base64-encoded but not encrypted; assume that anyone with the token can read every claim. Never put a password, an SSN, or a credit card number into a payload.
The third segment is the signature. To produce it, the auth server takes base64url(header) + "." + base64url(payload), computes a SHA-256 hash, and signs the hash with its private key. The verifier, holding only the matching public key, can recompute the hash, decrypt the signature, and check that the two match. If anyone changes one bit of the header or payload after the fact, the recomputed hash will not match the signature, and verification fails. That is the entire integrity story: the signature binds the contents.
The whole token is then encoded as base64url — standard base64 with two character substitutions to make it safe for URLs and HTTP headers. The encoding is reversible; it adds no security. The point is wire compatibility: a JWT can ride in an Authorization: Bearer header without escaping, in a query string without breaking URLs, in a cookie without confusing the cookie parser. Decode it with any JSON-aware tool and you read the claims directly.
Origins — JOSE, JWT, JWS, JWE, the spec family
A specification family that took a decade to settle.
JSON Web Token is defined by RFC 7519, published by the IETF in May 2015 after roughly four years of working-group drafts. The lead authors — Michael B. Jones (Microsoft), John Bradley, and Nat Sakimura — chaired or contributed to the broader JOSE (JSON Object Signing and Encryption) working group, which produced a coordinated set of specifications. RFC 7515 defines the JSON Web Signature container; RFC 7516 defines JSON Web Encryption; RFC 7517 defines JSON Web Key and the JWKS endpoint format; RFC 7518 defines the JSON Web Algorithms registry, listing valid alg values such as HS256, RS256, ES256, PS256, EdDSA (added in RFC 8037, 2017).
RFC 7519 is one of the IETF's shorter normative documents — thirty pages including examples — but it leaves a great deal to implementers. The seven registered claim names iss, sub, aud, exp, nbf, iat, jti are normative; the rest are private agreements between issuer and verifier. The spec is silent on token lifetime, on whether to use refresh tokens, on the JWKS rotation cadence, and most critically on which algorithms a verifier should accept — all of which became sources of vulnerability classes that took years to surface.
The follow-up RFC 8725 (February 2020), JSON Web Token Best Current Practices, exists precisely because the original spec's permissiveness produced a steady drumbeat of CVEs. BCP 8725 mandates pinning the expected algorithm at verification time, rejecting alg: none outright, validating audience and issuer, and constraining acceptable algorithms to a per-deployment allowlist. Anyone shipping a JWT verifier in 2020 or later is on notice; ignoring the BCP is the practical equivalent of deploying TLS with anonymous cipher suites enabled.
The JOSE family sits inside the larger OAuth 2.0 stack defined by RFC 6749 (October 2012) and the OpenID Connect Core specification (Sakimura et al., February 2014, with subsequent errata through 2023). OIDC ID tokens are JWTs; OAuth 2 access tokens may be JWTs or opaque, at the issuer's choice. The OAuth-Bearer-Token format described in RFC 6750 is agnostic, but in practice every modern issuer — Auth0, Okta, AWS Cognito, Keycloak, Azure AD / Entra ID, Google Identity Platform, GitHub Actions OIDC — emits JWTs as ID tokens, and most emit JWTs as access tokens too.
The token itself is three base64url-encoded segments separated by dots: header, payload, signature. The header is a small JSON object naming the algorithm and the key ID; the payload is a JSON object of claims; the signature covers the first two segments concatenated as base64url(header) + "." + base64url(payload). The encoding is deliberately URL-safe, which is what makes JWTs convenient as bearer tokens in HTTP Authorization headers and as fragments in OAuth implicit-flow URLs (the latter use case is now strongly discouraged in favour of authorization-code-with-PKCE since RFC 8252).
JWT vulnerabilities — "alg: none", confused-deputy, key confusion
"alg: none" was a real bug, and not the only one.
The most public JWT vulnerability class is algorithm confusion. JWT headers carry an alg field naming the signing algorithm; many libraries through 2015 trusted that field rather than the verifier's expectation. The none algorithm, defined in RFC 7518 for use only in cases where signature is provided by another mechanism, was treated as a valid value by libraries that did not opt out. An attacker could re-encode any payload with {"alg":"none"}, an empty signature, and have it accepted.
The Auth0 security team's 2015 disclosure, Critical vulnerabilities in JSON Web Token libraries, by Tim McLean, catalogued the issue across roughly two dozen libraries: node-jsonwebtoken, jose-jwt, php-jwt, ruby-jwt, pyjwt, java-jwt, and others. Two attack patterns: send alg: none with no signature, or send alg: HS256 using the verifier's public RSA key as the HMAC secret. The latter works because libraries that accept both symmetric (HMAC) and asymmetric (RSA, ECDSA) algorithms often had a single verify(token, key) entry point that picked the algorithm from the header and dispatched to the matching primitive — never noticing the algorithm mismatch.
Three years later, CVE-2018-1000531 hit a similar family of issues in the Node.js jsonwebtoken package version 4.2.1; CVE-2017-2800 targeted the C library cjose; CVE-2019-7644 targeted Auth0's own node-auth0; CVE-2022-21449 — the “Psychic Signatures” bug found by Neil Madden — let attackers forge ECDSA signatures against vulnerable Java versions (15–18) by submitting r=0, s=0 in the signature, which the JDK accepted as valid for any message. The pattern keeps recurring because the surface area is large: any combination of permissive defaults, optional algorithm validation, and shared verify entry points creates the same class.
The defence is uniform across modern libraries. Pin the expected algorithm at verifier-construction time; reject any token whose header disagrees; never call a verify function with a polymorphic key. The jose library by Filip Skokan (Node.js, Deno, Cloudflare Workers, browser) and jose4j by Brian Campbell (Java) are the modern reference implementations; PyJWT 2.x requires explicit algorithms= on every decode call. The Auth0 vulnerability writeup is mandatory reading; OWASP's JWT Cheat Sheet is the convenient reference.
JWT validation checklist
The validation checklist.
RFC 8725 plus the OWASP JWT Cheat Sheet collapse into a six-item checklist that a production verifier must pass. One: pin the algorithm. Construct the verifier with an explicit allowlist (typically a single algorithm: RS256 or ES256). Reject tokens whose header advertises anything else. Modern libraries make the algorithm a constructor argument; older ones expose it on every verify call.
Two: validate the signature against the right key. JWKS rotation works through a kid (key ID) in the JWT header pointing to one of several public keys at the issuer's /.well-known/jwks.json endpoint. Cache the JWKS for 5–60 minutes (Auth0's recommended default is 10 minutes); on a signature failure, re-fetch once before returning a final 401, in case the issuer rotated mid-request. Auth0, Okta, AWS Cognito, Azure AD, Google Identity Platform, and GitHub Actions all expose JWKS endpoints; key rotation is on the order of weeks to months, but rapid emergency rotation is the standard incident response.
Three: check exp and nbf. The expiration claim is required for any token whose lifetime matters; nbf (not-before) prevents pre-issued replay. Allow 30–60 seconds of clock-skew tolerance — AWS recommends 60s, Auth0 30s. Four: check iss and aud. The issuer must exactly match the configured authority URL; the audience must include this service's identifier. Without audience validation a token issued for service A is freely replayable at service B inside the same trust domain — a real and frequently exploited misconfiguration.
Five: validate the rest of the claims your service cares about. Scopes, roles, tenant identifiers, custom claims — whichever your authorization layer uses. Six: optional jti denylist. A small in-memory denylist (Redis, ElastiCache, an LRU cache) holds revoked-but-not-yet-expired JTIs. The denylist stays bounded because every entry expires when the token would have expired anyway; with 15-minute access tokens, the denylist holds at most one revocation-window's worth of entries.
The reference implementations of this checklist are worth studying. AWS Cognito's published verifier code (in JavaScript and Python on the Cognito docs site), Microsoft's Microsoft.IdentityModel.JsonWebTokens NuGet package, and Spring Security's NimbusJwtDecoder all exhibit the pattern: builder-based construction with pinned algorithm, JWKS-resolver wired in, mandatory issuer/audience configuration, and clock-skew exposed as a tunable. Hand-rolled verifiers are a recurring source of incidents; use a library that has been to a security audit at least once.
// Decoded JWT header — what every verifier must inspect first
{
"alg": "RS256", // pinned at verifier; reject if it disagrees
"typ": "JWT", // optional but conventional
"kid": "2024-08-01-key" // points to one entry in /.well-known/jwks.json
}
// Decoded payload (claims) — these are the fields the spec names
{
"iss": "https://accounts.example.com", // issuer — exact match required
"sub": "user-42", // subject (the user)
"aud": "https://api.example.com", // audience — must contain THIS api
"exp": 1714600000, // expiration (unix seconds)
"iat": 1714596400, // issued-at
"nbf": 1714596400, // not-before
"jti": "9f2c…", // unique id for denylisting
"scope": "read:invoices write:invoices"
}Self-describing vs authoritative tokens — when each fits
Self-describing or authoritative.
The most-asked question after “what is a JWT” is “should I use a JWT.” The honest answer is that both options have a real production story and both are correct in their context.
A JWT access token is self-describing: it carries the user identity, scopes, and expiration in its payload, and the resource server can verify it offline by checking the signature against the issuer's JWKS. The verification cost is roughly 50 µs for an Ed25519 signature, 80 µs for an ECDSA P-256 (ES256) signature, or 200 µs for an RSA-2048 (RS256) signature on a typical CPU — small enough to run on every request without caching. Wire size is 500–1500 bytes, dominated by the base64url-encoded payload and signature. Revocation is the pain point: once issued, a JWT remains valid until exp, regardless of subsequent password changes or session-invalidation events at the auth server.
An opaque token is a random 30–60 byte string — a database key. Verification means calling the issuer's introspection endpoint (RFC 7662, October 2015) on every request, or caching introspection results with a short TTL. Hot-path cost is dominated by the network round trip: 1–5 ms typical, often cached down to 50–200 µs in front of an in-memory store. Revocation is trivial: delete the row, every subsequent request fails closed. Wire size is 30 bytes.
The mature pattern in 2024-era systems combines both. Auth0, Okta, Keycloak, and AWS Cognito all default to JWT access tokens with 5–15 minute TTLs paired with longer-lived refresh tokens (30 days typical, with rotation on use). The short JWT TTL bounds revocation latency to the access-token lifetime; the refresh token, kept opaque or as a separate denylist-tracked JWT, is the surface where session invalidation actually happens. Stripe's API takes the opposite default: opaque API keys for all server-to-server traffic, JWTs only for short-lived browser sessions. Both architectures are defensible; the choice is about whether you can tolerate “fail closed in 15 minutes” revocation latency or whether you need instant lockout.
OIDC ID tokens are always JWTs because they are consumed by the client (a mobile app, a single-page app, a server-side relying party) which by design cannot call back to the issuer's introspection endpoint without re-establishing a trust path. The signature is the trust path. Access tokens, by contrast, are consumed by resource servers under the same trust domain as the issuer, and the design space for them is wider.
| alg | Family | Sig size | Verify cost | Notes |
|---|---|---|---|---|
| HS256 | HMAC-SHA256 (symmetric) | 32 B | ~1 µs | verifier needs the secret — bad for fan-out |
| RS256 | RSA PKCS#1 v1.5 + SHA-256 | 256 B | ~200 µs | most common; large signatures, slow signing |
| ES256 | ECDSA P-256 + SHA-256 | 64 B | ~80 µs | smaller, faster; CVE-2022-21449 hit Java 15–18 |
| EdDSA | Ed25519 (RFC 8037) | 64 B | ~50 µs | deterministic, side-channel resistant; preferred new builds |
| none | — no signature | 0 B | — | never accept; the 2015 Auth0 bug class |
Refresh tokens, rotation, and revocation
Long life, short reach.
The OAuth 2.0 refresh-token flow (RFC 6749 §6) was designed precisely to keep access tokens short-lived without forcing users to re-authenticate every quarter hour. The client receives an access token (5–15 minute TTL) and a refresh token (days to months) at login; when the access token expires, the client exchanges the refresh token for a new access token via a POST to /oauth/token with grant_type=refresh_token. The auth server verifies the refresh token, optionally rotates it (issues a new one and invalidates the old), and returns a fresh access token.
Refresh-token rotation is the modern best practice, codified in RFC 6819 (OAuth 2.0 Threat Model, January 2013) and elaborated in OAuth 2.1 drafts. Each use of a refresh token invalidates that specific token and issues a successor; presenting an already-redeemed refresh token is taken as evidence of theft and triggers session-wide revocation. Auth0's documentation on Refresh Token Rotation describes this as “automatic reuse detection”; Okta calls it “rotating refresh tokens”; the pattern is the same across implementations.
Sliding sessions, where each authenticated request extends the refresh-token expiration, are common in interactive products: Slack, Discord, and Notion all use a sliding-window scheme where a user who returns within the window stays logged in indefinitely. The trade-off is that the auth server must track per-session state — you cannot have a sliding session with purely stateless verification.
The device authorization flow (RFC 8628, August 2019) handles the case where the device requesting access has limited input (a TV, a CLI). The user is given a short code to enter on a phone or laptop; the device polls the auth server until the user completes consent. GitHub CLI, AWS CLI v2, kubectl-oidc-login, and most TV streaming apps use this flow. The resulting tokens are conventional JWTs and refresh tokens.
Notable failure modes: refresh tokens leaked from a compromised device permit indefinite session extension; bound refresh tokens (DPoP, RFC 9449, September 2023) tie refresh tokens to a per-client cryptographic key to mitigate this. The mTLS-bound token pattern of RFC 8705 (February 2020) achieves the same goal via client-certificate binding for higher-trust scenarios.
Auth0 recommends a 10-minute JWKS cache; on a signature-verification failure, re-fetch once before returning 401. A cache TTL longer than your access-token TTL means a key rotated for a security incident keeps verifying tokens past its retirement.
Access tokens and refresh tokens — two tokens, two purposes
Two tokens, two purposes.
OpenID Connect layers an authentication protocol on top of OAuth 2.0, and at its centre is the distinction between ID tokens and access tokens. An ID token answers the question “who is this user, and when did they authenticate?”; it is a JWT signed by the auth server, consumed by the client (the relying party), and never sent to a resource server. An access token answers “is this caller authorised for this resource?”; it is sent in the Authorization: Bearer header on every API call to a resource server.
OIDC Core 1.0 mandates that ID tokens contain iss, sub, aud, exp, iat, auth_time, and optionally nonce. The nonce claim binds the ID token to a specific authentication request, defeating replay across sessions; the at_hash claim binds the ID token to a specific access token issued in the same authentication, defeating cut-and-paste between flows. Both are checked by the relying-party client; both are routinely missed by hand-rolled OIDC clients.
The deployments matter. Sign-in with Apple uses ECDSA-signed ID tokens with a 15-minute TTL; Google Identity Platform uses RS256-signed ID tokens with a one-hour TTL; Microsoft Entra ID issues v2.0 ID tokens with one-hour TTL by default, configurable down to ten minutes via Conditional Access policies; GitHub Actions OIDC issues per-job ID tokens that runners exchange for AWS, GCP, or Azure credentials — the foundation of keyless CI/CD that has gradually replaced static cloud secrets in repos since 2021.
The cross-service confusion most teams encounter is conflating ID tokens with access tokens at the API layer. The 2018 Auth0 blog post ID Token and Access Token: What Is the Difference? by Andrea Chiarelli is the canonical clarification: ID tokens to the client, access tokens to the API, never the reverse. Resource servers that accept ID tokens as bearer authorization create a class of bugs because ID tokens often contain more sensitive personal data than access tokens (full email, name, profile picture URL) and were never intended to reach a resource server.
Two further claims warrant explicit attention. The azp (authorized party) claim, defined in OIDC Core, names the client that requested the token; it is required when an ID token has multiple audiences and is widely under-validated. The amr (authentication methods references) claim enumerates how the user proved identity — pwd, mfa, otp, hwk, face, fpt per RFC 8176 (June 2017). Resource servers that gate sensitive actions on step-up authentication read amr to enforce policies like “require mfa within the last five minutes for wire transfers,” an increasingly common pattern in fintech APIs.
When opaque tokens win — sessions, server-side state
The cases where opaque wins.
JWTs are the right answer for resource-server fan-out: many independent services, each verifying tokens locally with no shared session store, scaling horizontally with no introspection-endpoint hot path. They are a poor answer for several other shapes of system, and reaching for JWT in those cases produces mediocre architecture.
For browser sessions on a monolith, the classic server-side session cookie remains superior. A 30-byte session ID indexed against Postgres or Redis costs less to verify than a JWT signature, supports instant invalidation, and avoids the entire JWT-in-localStorage XSS attack surface. The 2017 Stop Using JWT for Sessions essay by Sven Slootweg (joepie91) makes this argument rigorously; the consensus of the security community since has been “cookies for sessions, JWTs for cross-service” even though the JWT-as-session pattern remains common in tutorials.
For cases that require instant revocation — admin role removal, compromised account, regulatory takedown — opaque tokens with introspection are the right tool. The 5 ms cost of an introspection call is acceptable in compliance-sensitive contexts; the JWT-with-denylist pattern works but is operationally heavier and tends to drift out of sync. PCI-DSS, HIPAA, and SOC 2 audits frequently flag JWT-with-long-TTL patterns where the auditors want to see immediate revocation paths.
For session-resumption on mobile, paired with biometric reauth or device attestation, the modern pattern is a refresh token bound to a hardware key (DPoP, mTLS, or platform attestation) rather than a long-lived JWT. The token itself can be opaque or JWT; what matters is the binding to a non-extractable private key on the device. Apple's Sign in with Apple, Google's Play Integrity, and the WebAuthn family of specifications point in this direction.
JWT also fits poorly when payloads are large (megabytes — consider sending an opaque reference and fetching the data over a regular API), when tokens must be short and human-readable (use a UUID or a Stripe-style prefixed key), or when the resource server is not under the same trust domain as the issuer (use OAuth Token Exchange, RFC 8693, January 2020, to mint a service-specific token). The most damning anti-pattern is JWT-as-database, where developers stuff complete user profiles into the payload and then discover that updating any of those fields requires re-issuing every active token. Tokens are claims at a moment in time; they should not double as a cache of the user record.
The pragmatic shape that has emerged across modern auth platforms is layered: JWT ID tokens for client-side identity, JWT access tokens with 5–15 minute TTLs for resource-server fan-out, refresh tokens (opaque or JWT, rotated on use, optionally DPoP-bound) for session continuity, and a small denylist for emergency revocation. Each layer has a clear responsibility; conflating them — using an ID token at an API, using an access token to identify a user in the UI, using a refresh token across services — is the source of most JWT incidents seen in incident reports from 2018 onward.
Further reading on JSON Web Tokens
Primary sources, in order.
- RFC 7519 · 2015JSON Web Token (JWT)The base spec. Short. The seven registered claim names — iss, sub, aud, exp, nbf, iat, jti — are all here.
- RFC 8725 · 2020JWT Best Current PracticesThe cautionary tale. Read this before shipping anything that issues or verifies JWTs.
- RFC 7517 · 2015JSON Web Key (JWK and JWKS)The key-distribution format every modern JWT verifier consumes from /.well-known/jwks.json.
- Sakimura et al · 2014OpenID Connect Core 1.0The authentication layer on top of OAuth 2.0. ID tokens, the nonce claim, the at_hash binding.
- McLean (Auth0) · 2015Critical vulnerabilities in JSON Web Token librariesThe disclosure that catalogued alg: none and the HS256/RS256 confusion across two dozen libraries.
- RFC 9449 · 2023DPoP — Demonstrating Proof-of-PossessionBinding tokens to a per-client key to mitigate refresh-token theft.
- OWASPJSON Web Token Cheat SheetThe condensed defensive checklist; updated continuously.
- Ferguson, Schneier, Kohno · 2010Cryptography Engineering — chapter on signatures and authenticationThe textbook treatment of how digital signatures work, the building block under every JWT.
- joepie91 · 2017Stop using JWT for sessionsThe widely cited critique of using JWTs where a session cookie would be simpler and safer.
- ComputerphileJWTs — how they work and whyA friendly fifteen-minute primer on the format, with the signature math drawn on a whiteboard. Useful as an entry point.
- Stanford CS253Web security — lecture notes on tokens and authenticationFeamster and Mickens' free Stanford course. Token-based auth as one piece of a wider web-security curriculum.
- Cloudflare blogCookies, sessions, and the modern token landscapeWhere JWT bearers fit alongside cookies, mTLS, and DPoP-bound tokens at the CDN edge.
- Semicolony guideOAuth 2 flowsWhere JWTs come from in practice — issued by an authorisation server, exchanged via grant types.
- Semicolony guideOpenID ConnectThe authentication layer; how ID tokens differ from access tokens and why it matters.