Identity & Access Management
Token-Based Sessions & JWTs
Stateful sessions (server-side store + opaque cookie) support instant revocation but need a shared store once there's more than one server; stateless sessions (signed tokens like JWTs) need no lookup but can't be revoked before they expire — a fundamental tradeoff, not a solved problem.
- Stateful sessions: the server holds session state, the client holds only an opaque id — revocation is instant (delete the row), but every server needs access to the same store (Stateless Services And Session Management), typically Redis, once there is more than one instance
- Stateless sessions: a signed token (JWT) carries its own claims, verified without a server-side lookup — cheap to scale horizontally, but a stolen valid token remains valid until it expires; there is no "delete the row"
- A JWT is
header.payload.signature, each part base64url-encoded — the payload is encoded, not encrypted, and readable by anyone holding the token; secrets never belong in a JWT payload - HS256 (HMAC, shared secret) requires every verifier to hold the same signing secret, which means every verifier could also forge tokens; RS256/ES256 (asymmetric) let any number of services verify with a public key while only the issuer holds the private signing key
- The standard mitigation for "can't revoke a JWT": short-lived access tokens (minutes) plus long-lived refresh tokens that rotate on every use, with reuse detection — a refresh token used twice signals theft and revokes the whole token family
- Storage is a genuine tradeoff, not a solved problem: HttpOnly+Secure+SameSite cookies are inaccessible to page JavaScript (resist XSS) but need CSRF defenses; localStorage is immune to CSRF but fully readable by any script on the page (any XSS exposes it) — no storage location defeats both attack classes
| Storage | XSS exposure | CSRF exposure | Cross-subdomain sharing |
|---|---|---|---|
| HttpOnly+Secure+SameSite cookie | Low — inaccessible to page JS | Needs explicit defenses (SameSite, CSRF token) | Easy via the Domain attribute |
| localStorage | High — any XSS can read and exfiltrate it | None — not sent automatically with requests | Not shared across subdomains by default |
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(issuerPublicKey)
.build(); // configured to accept ONLY RS256 — never trust the token's own "alg" header
Claims claims;
try {
claims = parser.parseClaimsJws(token).getBody();
} catch (SignatureException | ExpiredJwtException | MalformedJwtException e) {
throw new UnauthorizedException("token rejected: " + e.getMessage());
}
Instant expiry = claims.getExpiration().toInstant();
if (expiry.isBefore(Instant.now())) {
throw new UnauthorizedException("token expired");
}