What is a JWT?
A JWT (JSON Web Token) is a stateless, URL-safe token for securely transmitting claims between parties. Unlike session-based auth where the server stores session data, JWTs are self-contained — all information is encoded in the token itself. This makes JWTs ideal for distributed systems, microservices, and modern SPAs.
Token Anatomy
A JWT has three parts separated by dots: header, payload, and signature. The header declares the algorithm (HS256, RS256, etc.). The payload contains claims (user ID, roles, expiration). The signature proves the server created it. Importantly: the payload is BASE64-encoded but NOT encrypted — anyone with the token can read it. Never put passwords or secrets in the payload.
Signing Algorithms
- HS256 (HMAC with SHA-256): Symmetric — both parties share a secret. Fast, good for internal APIs. Use a strong, random secret.
- RS256 (RSA with SHA-256): Asymmetric — server signs with a private key, clients verify with a public key. Standard for OAuth 2 and public APIs.
- ES256 (ECDSA): Asymmetric, more efficient than RSA. Preferred for new systems.
Critical Vulnerabilities
The "none" Algorithm: Some libraries allow disabling signatures. An attacker can forge tokens. Always reject "none" server-side.
Weak Secrets: If you use HS256 with a guessable secret (password, environment name), attackers can brute-force and forge tokens. Use cryptographically random secrets, at least 32 bytes.
No Expiration Checks: Always validate the "exp" claim. Expired tokens should be rejected. If a token is compromised, expiration limits the window of vulnerability.
Leaking Sensitive Data: Because payloads are readable (not encrypted), never store passwords, SSNs, or payment info in a JWT.
Unencrypted Transport: Always use HTTPS. HTTP exposes tokens to interception. Set the Secure flag on JWT cookies to prevent transmission over plain HTTP.
Best Practices
1. Set Short Expiration: Access tokens should expire in 15 minutes. Use refresh tokens (valid for days or weeks) to renew access without re-authenticating. This limits damage if a token is stolen.
2. Validate on Every Request: Server-side verification is mandatory. Don't trust the token's claims; verify the signature using your key. Reject mismatched algorithms.
3. Use Libraries, Not Homemade Crypto: Implementing JWT signing/verification is error-prone. Use well-maintained libraries (jsonwebtoken for Node, PyJWT for Python, etc.).
4. Store Secrets Securely: Never hardcode secrets. Use environment variables or a secrets manager (Vault, AWS Secrets Manager). Rotate keys every 90 days or when team members leave.
5. Store Tokens Securely in Browsers: Avoid localStorage (vulnerable to XSS). Use memory or httpOnly cookies (inaccessible to JavaScript, immune to XSS).
6. Include Key ID (kid): Add a "kid" claim so the server knows which key was used. This enables key rotation without invalidating all tokens.
Real-World Flow: OAuth 2 with JWTs
- User clicks "Login with Google".
- App redirects to Google's auth endpoint.
- Google authenticates user and redirects back with an authorization code.
- App backend exchanges the code for an ID token (JWT) and access token.
- App verifies the ID token signature using Google's public keys.
- App issues its own short-lived access token to the frontend.
- Frontend includes the token in API requests (Authorization: Bearer token).
- Backend verifies the token on each request before serving.
Key Takeaways
- JWTs are signed, not encrypted. Server verifies the signature; anyone can read the payload.
- Use RS256 (asymmetric) for public APIs; HS256 (symmetric) for internal APIs with strong secrets.
- Always validate expiration and reject the "none" algorithm.
- Short-lived tokens (15 min) + refresh tokens are the gold standard.
- HTTPS is mandatory. Never log full tokens; log only the jti (JWT ID).
- Use a library, not homemade crypto. Rotate keys regularly.