JWT Deep Dive
debt(d7/e5/b5/t9)
Closest to 'only careful code review or runtime testing' (d7), because while semgrep and psalm are listed as detection tools and can catch specific patterns like base64_decode without verification or alg:none acceptance, these require custom rules and specialist configuration. The most dangerous variants (algorithm confusion, missing claim validation) are not caught by default linters and often slip through automated scanning unless rules are explicitly written for JWT misuse patterns — making careful code review the realistic detection path for most teams.
Closest to 'touches multiple files / significant refactor in one component' (e5), because the quick_fix involves not just a one-line patch but a coordinated set of changes: switching to a vetted library, whitelisting algorithms explicitly, adding claim validation (exp/iat/iss/aud), and potentially moving tokens from localStorage to HttpOnly cookies. This typically touches middleware, auth helpers, token issuance, and token validation code across multiple files in one component.
Closest to 'persistent productivity tax' (b5), because JWT auth applies to web and API contexts broadly and the chosen validation approach (algorithm whitelisting, claim validation strategy, storage mechanism) becomes a persistent pattern that shapes every authenticated endpoint and every future developer who touches auth code. It is not load-bearing across the entire system like a database choice, but it is a sustained tax on all auth-related work streams.
Closest to 'catastrophic trap' (t9), because the canonical misconception — that a valid JWT signature means the token is trustworthy — is precisely the 'obvious' belief that is always wrong. The term's misconception field states explicitly that the signature is only valid relative to a key and algorithm, and the alg:none and RS256→HS256 confusion attacks show that the intuitive trust in a 'signed' token leads directly to critical auth bypass. This contradicts how most developers reason about cryptographic signatures and the failure mode is silent, complete authentication bypass in production.
Also Known As
TL;DR
Explanation
JWT structure: header (algorithm + type), payload (claims: sub, exp, iat, iss, custom), signature (HMAC-SHA256 or RSA/ECDSA). Signed JWTs (JWS) prevent tampering but the payload is readable by anyone. Encrypted JWTs (JWE) also hide the payload. Critical security: always validate exp (expiry), iss (issuer), aud (audience). Never accept algorithm=none. Prefer RS256 (asymmetric) for public verification over HS256 (shared secret). Store in HttpOnly cookies, not localStorage.
Common Misconception
Why It Matters
Common Mistakes
- Not validating exp claim — expired tokens accepted indefinitely after logout.
- Accepting algorithm: none — allows unsigned tokens; always whitelist accepted algorithms explicitly.
- Algorithm confusion: RS256 → HS256 — attacker signs with public key as HMAC secret; validate alg header.
- JWT in localStorage — XSS-accessible; use HttpOnly cookies with SameSite=Strict.
Avoid When
- You need to invalidate tokens before expiry — JWTs are stateless and cannot be revoked without a server-side denylist.
- The payload contains sensitive data — JWT payloads are base64-encoded, not encrypted; anyone can decode them.
- Using the alg:none vulnerability window — always validate the algorithm header and reject none.
- Long-lived JWTs — a stolen token is valid until expiry; keep access tokens short (15 min) and use refresh tokens.
When To Use
- Stateless authentication across multiple services where a shared session store is not feasible.
- API authentication where the client stores the token and sends it with each request.
- Single sign-on (SSO) flows where identity is delegated from an auth server to resource servers.
- Short-lived service-to-service tokens where the overhead of a token introspection call is undesirable.
Code Examples
// Algorithm confusion vulnerability:
function validateJwt(string $token, string $publicKey): array {
$parts = explode('.', $token);
$header = json_decode(base64_decode($parts[0]), true);
$alg = $header['alg']; // Attacker sets alg=HS256
// Uses public key as HMAC secret — attacker can sign!
return verifySignature($token, $publicKey, $alg);
}
// Whitelist algorithm, validate all claims:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Always specify allowed algorithms explicitly:
$decoded = JWT::decode(
$token,
new Key($publicKey, 'RS256') // Algorithm whitelist — rejects HS256, none
);
// Validate critical claims:
if ($decoded->iss !== 'https://auth.example.com') throw new Exception('Invalid issuer');
if ($decoded->aud !== 'api.example.com') throw new Exception('Invalid audience');
if ($decoded->exp < time()) throw new Exception('Token expired');