By AndyPublished

JWT Security Best Practices

JWTs are secure when implemented correctly, but they introduce specific risks when key practices are missed. This guide covers the most important security requirements for production JWT systems — from token lifetime and storage to algorithm choice, key management, claim validation, and the vulnerabilities that appear most often in real-world JWT deployments.

1. Use Short Token Lifetimes

The exp claim is your primary defense against stolen tokens. If a token is intercepted or leaked, a short expiry limits the attacker's window of use.

  • ·Access tokens: 5–15 minutes for high-security APIs; up to 1 hour for general use
  • ·Refresh tokens: hours to days, stored securely server-side with revocation support
  • ·One-time tokens: seconds to a few minutes, with jti-based replay prevention

Never issue access tokens with multi-day or indefinite lifetimes. If users complain about re-authentication frequency, implement a refresh token flow rather than extending access token expiry. Refresh tokens are long-lived but can be revoked; access tokens are short-lived and stateless.

2. Store Tokens Securely

!
Storing JWTs in localStorage makes them accessible to any JavaScript on the page, including third-party scripts and XSS payloads. This is one of the most common JWT security mistakes in web applications.

Storage options and their tradeoffs:

  • ·HttpOnly cookies (recommended): The browser stores the token and sends it automatically. JavaScript cannot access it, so XSS cannot steal it. Use SameSite=Strict or SameSite=Lax to prevent CSRF. Pair with CSRF tokens for sensitive mutations.
  • ·sessionStorage: Cleared when the tab closes. More limited XSS exposure than localStorage but still accessible to JavaScript on the page. Better than localStorage, worse than HttpOnly cookies.
  • ·localStorage: Persistent, but accessible to all JavaScript on the page. Only use for non-sensitive tokens in applications where XSS risk is genuinely low.
  • ·Memory (in-app state): Safest against XSS, but lost on page refresh. Works well with silent refresh: keep a short-lived access token in memory and refresh it using a longer-lived HttpOnly refresh token cookie.

3. Restrict Accepted Algorithms

Always specify the allowed algorithms explicitly when configuring your JWT library. Never allow the library to accept any algorithm the token claims to use.

  • ·Never accept alg=none: This disables signature verification entirely. A library that accepts "none" can be exploited by anyone to forge tokens.
  • ·Prevent algorithm confusion: If you expect RS256, configure your library to only accept RS256. Do not allow HS256 as a fallback. The classic "RS256 to HS256" confusion attack lets an attacker sign a token with your public key (treated as an HMAC secret).
  • ·Use modern algorithms: Prefer RS256/RS384/RS512, ES256/ES384/ES512, or PS256/PS384/PS512. Avoid MD5 or SHA-1 based variants if your library offers them.

4. Validate All Required Claims

Signature verification is necessary but not sufficient. Your application must explicitly validate the following claims on every token it processes:

  • ·exp: Reject tokens where the current Unix time ≥ exp. Allow a small clock skew tolerance (≤ 60 seconds).
  • ·nbf (if present): Reject tokens where the current Unix time is before nbf.
  • ·iss: Reject tokens not issued by the expected issuer. Use exact string matching, not prefix matching.
  • ·aud: Reject tokens not intended for your service. Check that your service's identifier appears in the aud value.

5. Manage Keys Properly

HMAC Secrets (HS256)

  • ·Use at least 32 bytes of cryptographically random data as the secret
  • ·Store in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.), not in code or config files
  • ·Rotate on a schedule and immediately if compromised
  • ·Never log, transmit in URLs, or commit to source control

Asymmetric Keys (RS256/ES256)

  • ·Generate the private key in the auth service and never export it to other services
  • ·Publish the public key via a JWKS endpoint with caching headers
  • ·Implement key rotation: add a new key, issue tokens with the new kid, support both keys during the overlap period (until old tokens expire), then remove the old key
  • ·Protect private key storage with HSMs or cloud KMS in high-security environments

6. Keep Payloads Minimal and Safe

  • ·No secrets in claims: Passwords, API keys, database credentials, and private keys must never appear in JWT payloads. The payload is readable by anyone with the token.
  • ·Minimal PII: Include only the PII claims your service actually needs. Tokens often appear in logs; unnecessary PII increases data breach exposure.
  • ·Claims are a snapshot: JWT claims reflect the state at issuance. A user's roles or permissions could change after the token is issued. For privilege-sensitive operations, validate current permissions against your database rather than relying solely on token claims.

7. Token Revocation Strategies

Stateless JWTs cannot be revoked by default, there is no server-side record to delete. Three practical strategies exist, each with different tradeoffs:

Strategy 1: Short Expiry + Refresh Tokens (Recommended)

Issue short-lived access tokens (5–15 minutes) paired with longer-lived refresh tokens stored server-side. When you need to revoke a user's access, invalidate their refresh token. The next time their access token expires and they attempt a refresh, the request fails. The maximum window of continued access is the access token lifetime, typically 5–15 minutes.

This is the most common production approach. It preserves the stateless benefits of JWTs on the hot path (access token verification requires no database lookup) while enabling revocation via the refresh token store.

Strategy 2: Token Blocklist (Denylist)

Maintain a server-side store of revoked token identifiers (using the jti claim). On every request, check whether the token's jti appears in the blocklist. If it does, reject the token regardless of the signature and expiry.

This provides immediate revocation but reintroduces a database lookup on every API call — partially negating the stateless advantage of JWTs. The blocklist must be shared across all API instances and pruned periodically (entries can be removed once the token's exp has passed). Use Redis with TTL-based expiry for efficient implementation.

Strategy 3: Refresh Token Rotation

Each time a refresh token is used to get a new access token, issue a new refresh token and invalidate the old one. If a stolen refresh token is used, the legitimate user's next refresh will fail (because their token was already rotated), triggering an alert. This is rotation with reuse detection.

Rotation limits the window of exposure even if a refresh token leaks: the attacker and the legitimate user are now racing to use it, and whichever uses it first causes the other's token to fail. Implement detection logic that flags and revokes all of a user's tokens when rotation conflict is detected.

// Refresh token rotation, server-side pseudocode
async function refreshAccessToken(refreshToken: string) {
  const stored = await db.findRefreshToken(refreshToken)

  if (!stored) {
    // Token not found, might be a reuse attack
    // Revoke all tokens for this user as a precaution
    await db.revokeAllTokensForUser(stored?.userId)
    throw new Error('Invalid refresh token')
  }

  if (stored.used) {
    // Reuse detected, revoke everything for this user
    await db.revokeAllTokensForUser(stored.userId)
    throw new Error('Refresh token reuse detected')
  }

  // Mark old token as used, issue new pair
  await db.markRefreshTokenUsed(refreshToken)
  const newRefreshToken = await db.createRefreshToken(stored.userId)
  const accessToken = issueAccessToken(stored.userId)

  return { accessToken, refreshToken: newRefreshToken }
}

8. PKCE for Public Clients

PKCE (Proof Key for Code Exchange, RFC 7636) protects OAuth 2.0 authorization code flows in public clients, browser-based SPAs and mobile apps, where it is impossible to keep a client secret confidential. Without PKCE, an attacker who intercepts the authorization code can exchange it for tokens.

PKCE works by having the client generate a secret before the authorization request and prove possession of that secret during the token exchange:

// Step 1: Client generates a code verifier (random string, 43–128 chars)
const codeVerifier = generateRandomString(64)

// Step 2: Client computes the code challenge (SHA-256 hash of the verifier)
const codeChallenge = base64url(sha256(codeVerifier))

// Step 3: Client starts the authorization request with the challenge
GET /authorize?
  response_type=code
  &client_id=my-spa
  &redirect_uri=https://myapp.com/callback
  &code_challenge=<codeChallenge>
  &code_challenge_method=S256

// Step 4: After redirect, client exchanges code + original verifier
POST /token
  grant_type=authorization_code
  &code=<authorizationCode>
  &code_verifier=<codeVerifier>    ← server verifies SHA256(verifier) == challenge

// An attacker who intercepts the code cannot exchange it
// because they don't know the code_verifier

If an attacker intercepts the authorization code in the redirect, they cannot exchange it for tokens because they don't have the code_verifier: only the client that generated it holds that secret. PKCE is now recommended for all OAuth 2.0 flows, not just public clients (RFC 9700 extends it to confidential clients too).

All major identity providers support PKCE: Auth0, Okta, AWS Cognito, Google, and Azure AD all accept PKCE parameters. Your OAuth library should generate the verifier and challenge automatically, configure it to use code_challenge_method=S256 (SHA-256), never plain.

9. Common JWT Security Vulnerabilities

1. Algorithm Confusion (RS256 → HS256)

Some JWT libraries historically accepted any algorithm specified in the token header. An attacker who knows your RS256 public key can create a token with "alg": "HS256" in the header and sign it using the public key as the HMAC secret. A vulnerable verifier fetches the public key, then uses it as an HMAC secret (because the header says HS256) and the verification passes.

Fix: Always specify the allowed algorithm list explicitly in your library configuration. Never derive the verification algorithm from the token header.

2. alg=none Bypass

RFC 7519 defines "alg": "none" as a valid value for unsecured JWTs. Libraries that blindly trust the header's alg field will skip signature verification when the header claims none, accepting tokens with no signature at all. CVE-2015-9235 (node-jsonwebtoken) and similar CVEs in other libraries document this class of vulnerability.

Fix: Never include none in your allowed algorithm list. Any token claiming alg=none should be rejected immediately.

3. kid Header Injection

The kid (Key ID) header parameter is used to look up the signing key. If a library constructs a database query or file path directly from the kid value without sanitization, an attacker can inject a SQL payload or path traversal sequence into kid to manipulate key lookup. In the most severe cases, this allows an attacker to direct verification to use a key they control.

Fix: Never use kid values as raw input to database queries or file system lookups. Use a whitelist of known key IDs or look up keys from a pre-loaded JWKS only.

4. JWT Header Parameter Injection (jwk, jku, x5u)

The JWT spec allows optional header parameters that specify where to fetch the signing key:jwk (inline public key), jku (URL to JWKS), and x5u (URL to X.509 certificate). A vulnerable library that fetches keys from attacker-controlled URLs allows an attacker to embed their own public key in a forged token and have it accepted.

Fix: Ignore jwk, jku, and x5u header parameters in incoming tokens. Only use keys from your pre-configured JWKS endpoint or secret store. If your library supports it, disable these parameters explicitly.

5. Weak HMAC Secrets

HS256 tokens can be offline-brute-forced by an attacker who captures a token. Tools like hashcat can test billions of guesses per second against the token's signature. A secret derived from a password, UUID, or short string is vulnerable. The OWASP JWT Security Cheat Sheet documents this attack vector explicitly.

Fix: Generate HMAC secrets using a cryptographically secure random number generator with at least 256 bits (32 bytes) of entropy. Store secrets in a vault, not in environment variables committed to source control.

10. Additional Considerations

  • ·Implement token revocation for refresh tokens: While access token revocation is expensive, refresh token revocation is essential. Maintain a server-side refresh token store with invalidation support.
  • ·Use HTTPS everywhere: JWTs transmitted over plain HTTP can be intercepted. Enforce HTTPS with HSTS headers for all endpoints that issue or accept tokens.
  • ·Audit token issuance: Log when tokens are issued, to which user, for which audience, with which claims. Anomaly detection on token issuance patterns can catch credential stuffing and account takeover attempts.
  • ·Bind tokens to the client where possible: Token binding (tying a JWT to a specific TLS session or device fingerprint) limits the damage from token theft by making stolen tokens unusable from a different client context.
Security is a system property, not a single setting. Even a perfectly implemented JWT library provides weak security if the overall architecture is flawed. Review your full authentication and authorization flow holistically.
Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder