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
JWT storage locations, by exposure
StorageXSS exposureCSRF exposureCross-subdomain sharing
HttpOnly+Secure+SameSite cookieLow — inaccessible to page JSNeeds explicit defenses (SameSite, CSRF token)Easy via the Domain attribute
localStorageHigh — any XSS can read and exfiltrate itNone — not sent automatically with requestsNot shared across subdomains by default
Refresh-token rotation with reuse detection
Reusing an already-rotated refresh token is a theft signal, not a legitimate race — the whole family gets revoked, not just the reused token
Verifying a JWT's signature, pinned algorithm, and expiry
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");
}
A parser must be configured with the expected algorithm ahead of time — deriving it from the token itself is exactly the algorithm-confusion vulnerability below
Sources
  • API Security in ActionCh. 6 — Session Cookie and Token Authentication
  • OAuth 2 in ActionCh. 6 — Tokens