JWT Security Best Practices for Web Developers

JWTs are a powerful tool for authentication and authorization, but their security depends entirely on how they are implemented. A misconfigured JWT system can expose your application to token forgery, privilege escalation, and account takeover. This guide covers the most important security practices you should follow when working with JSON Web Tokens, along with the common vulnerabilities you need to defend against.

Algorithm Validation

The most critical rule in JWT security is to always validate the algorithm on the server side. Never trust the alg header in an incoming token to determine which algorithm to use for verification. Instead, your server should enforce a specific, pre-configured algorithm.

The infamous "alg": "none" attack exploits libraries that accept unsigned tokens when the header declares no algorithm. If your verification code blindly trusts the algorithm field, an attacker can craft a token with alg: none, remove the signature entirely, and gain unauthorized access.

// DANGEROUS - trusts the token's algorithm claim
jwt.verify(token, secret);

// SAFE - explicitly specify the allowed algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });

Secret and Key Strength

For HMAC-based algorithms (HS256, HS384, HS512), the signing secret must be strong enough to resist brute-force attacks. RFC 7518 recommends that the secret be at least as long as the hash output — meaning at least 256 bits (32 bytes) for HS256. A short or guessable secret renders your entire authentication system vulnerable.

  • Use a cryptographically random secret, not a human-readable password.
  • For HS256, use at least 32 bytes of random data.
  • For HS384, use at least 48 bytes. For HS512, use at least 64 bytes.
  • Store secrets in environment variables or a secrets manager, never in source code.
  • Rotate secrets periodically and support multiple active keys during transitions.

For RSA and ECDSA algorithms, use key sizes that meet current security standards: at least 2048-bit RSA keys (preferably 4096-bit) and P-256 or P-384 curves for ECDSA.

Token Expiration Strategy

Every JWT should include an exp (expiration) claim, and your server must reject expired tokens. Short-lived tokens limit the damage window if a token is compromised. Common expiration times are:

  • Access tokens: 5 to 15 minutes for high-security applications, up to 1 hour for lower-risk APIs.
  • Refresh tokens: Hours to days, stored securely and used only to obtain new access tokens.
  • ID tokens: Typically match the access token lifetime or session duration.

Always validate exp with a small clock skew tolerance (a few seconds) to account for minor time differences between servers.

The Refresh Token Pattern

Short-lived access tokens provide security, but requiring users to log in every few minutes is unacceptable. The refresh token pattern solves this:

1. User logs in → receives access token (15 min) + refresh token (7 days)
2. Client uses access token for API requests
3. Access token expires → client sends refresh token to /token/refresh
4. Server validates refresh token → issues new access token
5. Refresh token expires → user must log in again

Refresh tokens should be opaque strings (not JWTs) stored in a server-side database. This allows you to revoke them instantly, unlike stateless JWTs. Store refresh tokens in httpOnly cookies or secure server-side storage, never in localStorage.

Audience and Issuer Validation

Always validate the iss (issuer) and aud (audience) claims. Without these checks, a token issued by one service could be replayed against a different service in your organization.

jwt.verify(token, secret, {
  algorithms: ['RS256'],
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com'
});

If your system has multiple APIs, each API should check that the aud claim includes its own identifier. This prevents a token meant for the billing API from being used to access the admin API.

JWT Storage Security

Where you store JWTs in the browser involves a tradeoff between two attack vectors:

  • localStorage → XSS risk: Any JavaScript on the page can read the token. A single XSS vulnerability lets an attacker steal tokens.
  • httpOnly cookies → CSRF risk: The cookie is sent automatically with requests, so an attacker could trick a user's browser into making authenticated requests from a malicious site.

The recommended approach for most applications is httpOnly cookies with SameSite=Strict or SameSite=Lax, combined with CSRF tokens for state-changing operations. Also set the Secure flag so cookies are only sent over HTTPS.

Common Vulnerabilities

Algorithm Confusion Attack

When an application uses an asymmetric algorithm (like RS256) but does not enforce it, an attacker can change the header to HS256 and sign the token with the server's public key (which is often publicly available). If the server uses the public key as the HMAC secret, the forged token will pass verification. The fix: always whitelist the expected algorithm in your verification code.

kid Injection

The kid (Key ID) header parameter tells the server which key to use for verification. If the server uses kid directly in a database query or file path lookup without sanitization, an attacker could inject SQL or path traversal payloads. Always validate and sanitize the kid value before using it to look up keys.

JWK Header Injection

Some libraries allow a jwk parameter in the JWT header that embeds the verification key directly in the token. An attacker can generate their own key pair, embed their public key in the header, and sign the token with their private key. The server then uses the attacker-supplied key to verify the attacker-signed token. Disable jwk header support and always use server-side key management.

Token Revocation Approaches

Because JWTs are stateless, the server does not track active tokens by default. If a token is compromised, you need a way to invalidate it before it expires:

  • Blocklist: Maintain a server-side list of revoked token IDs (jti). Check each incoming token against this list. Use an in-memory cache like Redis for performance.
  • Short expiry + refresh tokens: Keep access tokens very short-lived (5-15 minutes) so compromised tokens expire quickly. Revoke the refresh token on the server side to prevent new access tokens from being issued.
  • Token versioning: Store a version counter on the user record. Include the version in the JWT. When you need to revoke all tokens for a user, increment the version. Tokens with the old version are rejected.

JOSE Header Best Practices

The JOSE (JSON Object Signing and Encryption) header controls how the token is processed. Follow these guidelines:

  • Only include necessary header parameters: alg, typ, and kid when using key rotation.
  • Set typ: "JWT" explicitly to prevent cross-JWT confusion attacks where different token types might be accepted interchangeably.
  • Reject tokens with unrecognized or unnecessary header parameters.
  • Never embed keys in the header ( jwk, x5c, x5u, jku) in production — use server-side key resolution instead.

Security is not a feature you add once — it requires ongoing vigilance. Keep your JWT libraries updated, monitor for new vulnerabilities, and regularly audit your token handling code.

Related Guides