Back to DevStash

JWT Authentication: Security Best Practices

Learn how JSON Web Tokens work, common security pitfalls, and proven patterns for protecting your APIs and user sessions.

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

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

  1. User clicks "Login with Google".
  2. App redirects to Google's auth endpoint.
  3. Google authenticates user and redirects back with an authorization code.
  4. App backend exchanges the code for an ID token (JWT) and access token.
  5. App verifies the ID token signature using Google's public keys.
  6. App issues its own short-lived access token to the frontend.
  7. Frontend includes the token in API requests (Authorization: Bearer token).
  8. Backend verifies the token on each request before serving.

Key Takeaways

Related Tools