Working with JSON Web Tokens (JWT)
1. Understanding JWT Structure
HEADER.PAYLOAD.SIGNATURE
eyJhbGciOi... eyJzdWIi... 3xJk2P...
Header : { "alg": "RS256", "typ": "JWT", "kid": "key-2026-01" }
Payload : { "sub": "user_42", "exp": 1715000000, ... }
Sig : sign(base64url(header) + "." + base64url(payload), key)
2. Creating JWT
Example: jsonwebtoken (Node)
import jwt from "jsonwebtoken";
const token = jwt.sign(
{ sub: user.id, role: user.role, scope: "read write" },
privateKey,
{
algorithm: "RS256",
issuer: "https://auth.example.com",
audience: "api://orders",
expiresIn: "15m",
keyid: "key-2026-01"
}
);
3. Validating JWT
| Step | Detail |
| Parse header | Read alg, kid |
| Pin algorithm | Reject if not in allowed list (prevent alg: none) |
| Fetch key | From JWKS endpoint by kid |
| Verify signature | Crypto check |
| Validate claims | iss, aud, exp, nbf, iat |
| Check revocation | jti against blacklist if applicable |
4. Using JWT Claims
| Claim | Meaning | Example |
| iss | Issuer | https://auth.example.com |
| sub | Subject (user ID) | user_42 |
| aud | Audience | api://orders |
| exp | Expiration (Unix sec) | 1715000000 |
| nbf | Not before | 1714999000 |
| iat | Issued at | 1714999000 |
| jti | Unique token ID | UUID |
| azp | Authorized party (OIDC) | client_id |
5. Implementing Custom Claims
| Practice | Detail |
| Namespace | Use URI to avoid collisions: "https://app/roles" |
| Minimize | Only what API needs (tokens travel) |
| No PII | JWT is signed not encrypted — readable |
| Size | Keep payload < 1KB to fit headers |
6. Choosing Signing Algorithms
| Algorithm | Key Type | Use Case |
| RS256 | RSA 2048+ | Most common, broad support |
| ES256 | ECDSA P-256 | Smaller sig, modern PREFERRED |
| EdDSA (Ed25519) | Curve25519 | Fast, modern |
| PS256 | RSA-PSS | FIPS environments |
| HS256 | Shared secret | Single trust domain only |
| none DEPRECATED | — | NEVER allow |
7. Using Symmetric vs Asymmetric Keys
| Aspect | Symmetric (HS*) | Asymmetric (RS*/ES*) |
| Key | One shared secret | Private + public |
| Verifier needs | Same secret | Public key only (JWKS) |
| Distribution | Hard at scale | Easy (JWKS endpoint) |
| Best for | Same-service tokens | Multi-service, federated |
8. Setting JWT Expiration
| Token Use | exp Recommendation |
| Access | 5–15 min |
| ID Token | 5–60 min |
| Service token | 1–5 min |
| Email link | 15–60 min |
| Clock skew | Allow ±60 sec leeway |
9. Implementing JWT Refresh Tokens
Note: Refresh tokens themselves should NOT be JWTs — use opaque random strings stored server-side for easy revocation.
10. Preventing JWT Vulnerabilities
| Vulnerability | Mitigation |
alg: none | Pin allowed algorithms server-side |
| Algorithm confusion (RS→HS) | Validate alg matches key type |
| Weak HS256 secret | ≥256-bit random key |
kid injection | Validate kid against allowlist |
| JWK header injection | Ignore embedded jwk/x5u claims |
| Replay | Short exp + jti uniqueness |
| No signature verify | NEVER use jwt.decode() for trust |
| Stored in localStorage | Use cookies (BFF) for browsers |
11. Storing JWTs
| Location | XSS | CSRF | Verdict |
| localStorage | Stolen | Safe | Avoid for sensitive |
| sessionStorage | Stolen | Safe | Avoid |
| Memory only | Lost on reload | Safe | OK with refresh |
| HttpOnly cookie | Safe | Need SameSite+CSRF | Best for SPAs |
12. Using JWE for Encrypted Tokens
| Aspect | JWE (RFC 7516) |
| Purpose | Confidentiality (payload encrypted) |
| Algorithms | RSA-OAEP, ECDH-ES (key wrap) + A256GCM (content) |
| Structure | 5 parts: header.encKey.iv.ciphertext.tag |
| When to use | Tokens containing sensitive PII / claims |
| Pattern | Nested: JWS-then-JWE (sign then encrypt) |