Skip to content

JWT Security Best Practices

JSON Web Tokens (JWTs) are widely used for authentication and authorization in APIs. When implemented incorrectly they become a high-severity attack surface. This guide covers the most important best practices and links each point to the relevant VulnAPI scan.

Prefer asymmetric algorithms (RS256, ES256) for distributed systems where multiple services verify tokens but only one issues them. This ensures the private key never leaves the issuer.

  • Recommended: RS256, RS384, RS512, ES256, ES384
  • Acceptable for single-service systems: HS256, HS384, HS512 (with a strong secret — see below)
  • Never use: alg: none in production

Using alg: none removes all cryptographic protection, making tokens trivially forgeable. VulnAPI detects this with the jwt.alg_none scan.

Use a Minimum 256-bit Random Secret for HMAC

Section titled “Use a Minimum 256-bit Random Secret for HMAC”

When using HMAC algorithms (HS256/HS384/HS512), the secret key must be:

  • At least 256 bits (32 bytes) of cryptographically random data
  • Not a dictionary word, well-known default, or guessable string
  • Not an empty string

Tokens signed with empty or weak secrets are detected by the jwt.blank_secret and jwt.weak_secret scans.

Terminal window
# Generate a secure 256-bit secret (Linux/macOS)
openssl rand -base64 32

Never trust a JWT without verifying its signature using the correct key. Skipping signature verification is one of the most dangerous implementation mistakes.

VulnAPI detects this with the jwt.not_verified scan (CVSS 9.3).

# Python example using PyJWT — always pass algorithms explicitly
import jwt
payload = jwt.decode(token, public_key, algorithms=["RS256"])

Some JWT libraries accept tokens whose signature segment is empty (null signature). Always configure your library to require a valid, non-empty signature.

VulnAPI detects this with the jwt.null_signature scan.

After verifying the signature, validate these claims before trusting any payload data:

ClaimDescriptionAction
issIssuer — who created the tokenReject if not from a trusted issuer
audAudience — who the token is intended forReject if your service is not the intended audience
expExpiration timeReject if current time is past exp
nbfNot-before timeReject if current time is before nbf
subSubject — who the token representsVerify the user still exists / is active

Failure to validate iss and aud enables cross-service relay attacks.

Use Short Token Lifetimes With Refresh Token Rotation

Section titled “Use Short Token Lifetimes With Refresh Token Rotation”

Short-lived access tokens limit the damage from a stolen token:

  • Access tokens: 5–15 minutes
  • Refresh tokens: hours to days, single-use only (rotate on each use)

Rotate refresh tokens on every use to detect token theft (if an already-used refresh token is presented, revoke the entire session).

The JWT specification allows alg: none for unsecured tokens (e.g., locally exchanged messages). This must never be accepted by any production API endpoint.

Harden your JWT library to reject none explicitly:

// Go example using golang-jwt
token, err := jwt.Parse(tokenString, keyFunc, jwt.WithValidMethods([]string{"RS256", "ES256"}))
StorageXSS riskCSRF riskRecommendation
localStorageHigh (JS readable)NoneAvoid for sensitive tokens
sessionStorageHigh (JS readable)NoneAvoid for sensitive tokens
HttpOnly cookieNone (not JS readable)Medium (mitigate with SameSite)Preferred
Memory (JS variable)LowNoneAcceptable for SPAs

Prefer HttpOnly; Secure; SameSite=Strict cookies for web applications. See HTTP Cookies Misconfiguration for related checks.