What Is a JWT and Why Security Matters
A JSON Web Token (JWT) is a compact, URL-safe token format used for securely transmitting claims between two parties. JWTs are the backbone of modern authentication and authorization. When you log in to a web application, the server typically issues a JWT that your browser sends with every subsequent request to prove your identity.
The reason JWT security matters so much is that a stolen or forged JWT gives an attacker full access to whatever that token authorizes. Unlike session IDs stored in a server-side database, JWTs are self-contained. The server does not look up a session -- it trusts the token itself. If an attacker can forge a valid token or steal one from a user, they can impersonate that user without the server ever knowing something is wrong.
JWT adoption has exploded across the industry. APIs, single-page applications, mobile apps, microservices, and OAuth 2.0 flows all rely on JWTs. This widespread usage means that JWT security best practices are not optional knowledge -- they are a requirement for any developer building authenticated systems.
The good news is that JWTs are secure when implemented correctly. The bad news is that the default settings in most JWT libraries are not secure enough for production use. This guide covers the specific vulnerabilities you need to defend against and the exact implementation patterns that keep your tokens safe.
JWT Structure Explained
Before you can secure a JWT, you need to understand what you are protecting. Every JWT consists of three base64url-encoded parts separated by dots. You can inspect any JWT using a JWT debugger to see these parts clearly.
Header
The header is a JSON object that declares the token type and the signing algorithm. It tells the verifier how to validate the signature.
{
"alg": "HS256",
"typ": "JWT"
}
The alg field is critical for security. It determines which cryptographic algorithm was used to sign the token. Common values include HS256 (HMAC with SHA-256), RS256 (RSA with SHA-256), and ES256 (ECDSA with P-256). The value none means the token is unsigned -- and this is where one of the most dangerous vulnerabilities lives.
Payload
The payload contains the claims -- the actual data the token carries. Claims are key-value pairs that describe the user, the token's validity period, and any other metadata your application needs.
{
"sub": "1234567890",
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "admin",
"iat": 1738152000,
"exp": 1738152900,
"iss": "https://auth.example.com",
"aud": "https://api.example.com"
}
Standard claims defined by the JWT specification (RFC 7519) include:
sub(Subject) -- Who this token represents, typically a user IDiat(Issued At) -- Unix timestamp of when the token was createdexp(Expiration) -- Unix timestamp after which the token is invalidiss(Issuer) -- Who created and signed the tokenaud(Audience) -- Who the token is intended fornbf(Not Before) -- Token is not valid before this timestampjti(JWT ID) -- Unique identifier for the token, useful for revocation
The JWT payload is encoded, not encrypted. Anyone who intercepts the token can decode and read the payload. Never store passwords, credit card numbers, API keys, or other secrets in a JWT payload. Use a JWT decoder to verify what your tokens actually expose.
Signature
The signature is what makes a JWT trustworthy. It is created by taking the encoded header and payload, combining them with a dot separator, and signing the result with the specified algorithm and a secret key (for HMAC) or a private key (for RSA/ECDSA).
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
When a server receives a JWT, it recomputes the signature using its own key and compares it to the signature in the token. If they match, the token has not been tampered with. If they do not match, the token is rejected. This is why protecting your signing key is the single most important aspect of JWT token security.
Common JWT Vulnerabilities
Understanding the attack surface is the first step toward a secure JWT implementation. These are the vulnerabilities that security researchers and penetration testers exploit most frequently.
1. The "none" Algorithm Attack
This is the most famous JWT vulnerability and the one you are most likely to encounter in security audits. The JWT specification allows an algorithm value of "none", which means the token has no signature at all. This was intended for situations where the token's integrity is already guaranteed by other means, such as a TLS connection between trusted internal services.
The attack works like this: an attacker takes a valid JWT, modifies the payload (for example, changing "role": "user" to "role": "admin"), sets the header algorithm to "none", removes the signature, and sends it to the server. If the server's JWT library blindly trusts the algorithm specified in the header, it will accept the token without any signature verification.
// Vulnerable: trusts the algorithm from the token header
const decoded = jwt.verify(token, secret);
// Secure: explicitly specifies allowed algorithms
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
Every modern JWT library supports an explicit algorithm allowlist. Use it. Always.
2. Weak Signing Secrets
When using HMAC-based algorithms like HS256, the security of every token depends entirely on the strength of the secret. If the secret is short, predictable, or commonly used, an attacker can brute-force it. Tools like jwt_tool and hashcat can attempt millions of candidate secrets per second against a captured token.
// DANGEROUS: weak, guessable secrets
const secret = "secret";
const secret = "password123";
const secret = "my-jwt-secret";
const secret = "company-name";
// SECURE: cryptographically random, 256+ bit secret
const secret = "xK9#mQ$vL2@pR8&nT5*wJ7^cF3!hB6%dY0+sA4";
For HMAC-SHA256, the secret should be at least 256 bits (32 bytes) of cryptographically random data. Generate it with openssl rand -base64 32 or the equivalent in your language, and store it in an environment variable or secrets manager -- never hardcode it in source code.
3. Token Leakage
Even a perfectly signed JWT is useless for security if it leaks to an attacker. Common leakage vectors include:
- URLs -- Passing JWTs as query parameters (
?token=eyJ...) exposes them in browser history, server logs, referrer headers, and proxy logs - Unencrypted connections -- Sending JWTs over plain HTTP allows network-level interception
- JavaScript-accessible storage -- Storing JWTs in
localStorageorsessionStoragemakes them available to any XSS payload - Verbose error messages -- Returning the token in error responses or debug output
- Log files -- Logging full request headers, including the
Authorizationheader, writes tokens to disk in plaintext
4. Missing Expiry Validation
A JWT without an expiration claim (exp) is valid forever. If it is ever stolen, the attacker has permanent access. Always set an expiration, and always validate it on the server side. Do not rely on the client to discard expired tokens.
5. Algorithm Confusion (Key Confusion)
This subtle attack targets systems that support both symmetric (HS256) and asymmetric (RS256) algorithms. When a server uses RS256, it verifies tokens with a public key. An attacker can change the algorithm to HS256 and sign the token using the public key as the HMAC secret. Since the public key is often available, the attacker can forge valid tokens.
The fix is the same as for the "none" algorithm attack: always enforce the expected algorithm on the server side and never let the token header dictate the verification method.
JWT Security Best Practices
These are the implementation patterns that protect your tokens in production. Treat them as a checklist for every system that uses JWTs.
1. Always Validate the Algorithm
Configure your JWT library to only accept the specific algorithm(s) you use. Reject any token that specifies a different algorithm, including none.
// Node.js (jsonwebtoken library)
const options = {
algorithms: ['RS256'], // Only accept RS256
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
};
const decoded = jwt.verify(token, publicKey, options);
# Python (PyJWT library)
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"], # Explicit allowlist
audience="https://api.example.com",
issuer="https://auth.example.com"
)
2. Use Short Expiration Times
Keep access tokens short-lived. The shorter the lifetime, the smaller the window of opportunity if a token is compromised.
- Access tokens: 5-15 minutes for most applications
- Sensitive operations: 1-5 minutes for financial or administrative actions
- Refresh tokens: 1-7 days, stored securely and revocable
3. Validate All Standard Claims
Do not just verify the signature. Validate exp (not expired), iss (issued by your auth server), aud (intended for your service), and nbf (valid by now). Missing any of these checks leaves an opening for token misuse.
4. Use Strong Signing Keys
- HMAC (HS256): Minimum 256-bit (32-byte) cryptographically random secret
- RSA (RS256): Minimum 2048-bit key pair, prefer 4096-bit
- ECDSA (ES256): Use P-256 curve or stronger
# Generate a strong HMAC secret
openssl rand -base64 32
# Generate an RSA key pair
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem
# Generate an ECDSA key pair (P-256)
openssl ecparam -genkey -name prime256v1 -noout -out ec-private.pem
openssl ec -in ec-private.pem -pubout -out ec-public.pem
5. Never Put Sensitive Data in the Payload
The JWT payload is base64url-encoded, not encrypted. Anyone with the token can read the claims. Store only identifiers and non-sensitive metadata in the payload. Keep passwords, payment information, and personal data in your database, referenced by the sub claim.
6. Implement Key Rotation
Signing keys should be rotated periodically. Use the kid (Key ID) header claim to identify which key was used to sign each token. Maintain a short overlap period where both the old and new keys are accepted for verification, then retire the old key.
For every JWT implementation, verify that you: enforce a specific algorithm, set short expiration times, validate issuer and audience claims, use cryptographically strong keys, store keys in a secrets manager (not source code), and never put sensitive data in the payload.
Secure Token Storage on the Client
Where you store JWTs on the client side has a direct impact on your application's security posture. The two primary options are cookies and web storage, and they have very different risk profiles.
HttpOnly Cookies (Recommended)
Store JWTs in cookies with the following flags:
HttpOnly-- Prevents JavaScript from reading the cookie, neutralizing XSS token theftSecure-- Cookie is only sent over HTTPSSameSite=Strict(orLax) -- Prevents the cookie from being sent in cross-site requests, mitigating CSRFPath=/-- Restrict the cookie to relevant paths if possible
// Express.js: Set JWT as HttpOnly cookie
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/'
});
localStorage and sessionStorage (Higher Risk)
Web storage is accessible to any JavaScript running on the page. A single XSS vulnerability -- a malicious script injected through an unescaped user input, a compromised third-party library, or a rogue browser extension -- can read and exfiltrate every token stored in localStorage. If you must use web storage, implement a strict Content Security Policy (CSP), sanitize all inputs, and audit every third-party script.
Refresh Token Strategy
Short-lived access tokens are useless without a way to get new ones without forcing the user to log in again. Refresh tokens solve this problem, but they introduce their own security considerations.
How Refresh Tokens Work
- User authenticates and receives both an access token (short-lived, 15 minutes) and a refresh token (longer-lived, 7 days)
- The access token is used for API requests until it expires
- When the access token expires, the client sends the refresh token to a dedicated endpoint
- The server validates the refresh token and issues a new access token
- Optionally, the server issues a new refresh token and invalidates the old one (rotation)
// Refresh token endpoint (Express.js)
app.post('/auth/refresh', (req, res) => {
const { refreshToken } = req.cookies;
// Verify the refresh token
const payload = jwt.verify(refreshToken, REFRESH_SECRET, {
algorithms: ['HS256']
});
// Check if the refresh token has been revoked
if (isRevoked(refreshToken)) {
return res.status(401).json({ error: 'Token revoked' });
}
// Issue new access token
const accessToken = jwt.sign(
{ sub: payload.sub, role: payload.role },
ACCESS_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
// Rotate: issue new refresh token, revoke old one
const newRefreshToken = jwt.sign(
{ sub: payload.sub },
REFRESH_SECRET,
{ algorithm: 'HS256', expiresIn: '7d' }
);
revoke(refreshToken);
res.cookie('access_token', accessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.cookie('refresh_token', newRefreshToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ message: 'Tokens refreshed' });
});
Refresh Token Security Rules
- Store server-side: Keep a record of issued refresh tokens in your database so you can revoke them
- Rotate on use: Issue a new refresh token each time one is used, and invalidate the old one
- Detect reuse: If a revoked refresh token is used, immediately revoke all tokens for that user -- it means the token was likely stolen
- Bind to device: Associate refresh tokens with a device fingerprint or IP range to limit lateral movement
Debugging JWTs Safely
Developers frequently need to inspect JWTs during development, debugging, and incident response. The key rule is simple: never paste production tokens into third-party websites that send data to a server.
Use Client-Side Debugging Tools
Tools that decode JWTs entirely in the browser are safe for inspecting any token, including production tokens. NexTool JWT Debugger processes everything client-side -- your token never leaves your machine. You can verify this by checking the network tab in your browser's developer tools.
Use a JWT decoder to quickly inspect the header and payload claims. Check the exp claim to see if the token has expired, verify the iss and aud claims match your expectations, and confirm the algorithm in the header is what your server expects.
Decode in the Terminal
You can decode the payload of a JWT directly in your terminal without any external tools:
# Decode the payload (second part) of a JWT
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0" | \
base64 -d 2>/dev/null | python -m json.tool
# Or use jq for cleaner output
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0" | \
base64 -d 2>/dev/null | jq .
Generate Test Tokens Safely
When testing JWT-based authentication, generate dedicated test tokens instead of using real user tokens. The NexTool JWT Generator lets you create tokens with custom claims and algorithms, entirely in the browser. This avoids the risk of accidentally leaking a production token in a test log or a screenshot.
Debug Your JWTs Right Now
Decode, inspect, and validate JWT tokens entirely in your browser. No data is sent to any server.
Open NexTool JWT DebuggerSecure JWT Implementation Checklist
This is the complete checklist for production-ready JWT security. Review it before deploying any JWT-based authentication system.
Token Creation
- Use a well-maintained, audited JWT library (jsonwebtoken for Node.js, PyJWT for Python, java-jwt for Java)
- Set
expto 5-15 minutes for access tokens - Include
iss,aud,sub, andiatclaims - Use
jtifor unique token identification when revocation is needed - Sign with RS256 or ES256 for distributed systems, HS256 for single-service architectures
- Store signing keys in environment variables or a secrets manager
Token Verification
- Explicitly specify allowed algorithms -- never trust the token header
- Validate
exp,iss,aud, andnbfclaims - Reject tokens with clock skew greater than 30 seconds
- Check revocation status for sensitive operations
- Return generic error messages (do not reveal why a token was rejected)
Infrastructure
- Enforce HTTPS everywhere -- never transmit JWTs over unencrypted connections
- Set a strict Content Security Policy to mitigate XSS
- Implement rate limiting on authentication and refresh endpoints
- Monitor for unusual token patterns (mass refresh requests, tokens from unexpected IPs)
- Rotate signing keys on a regular schedule and after any suspected compromise
- Log authentication events (token issued, refreshed, revoked) without logging the tokens themselves
JWT security is not about the format -- it is about the implementation. JWTs are as secure as the code that creates, transmits, stores, and verifies them. Follow this checklist, use established libraries, and test your implementation with tools like the NexTool JWT Debugger before going to production.
Frequently Asked Questions
What is the most common JWT security vulnerability?
The most common JWT security vulnerability is the "none" algorithm attack, where an attacker modifies the token header to bypass signature verification. This works when servers accept the algorithm specified in the token header without validating it against an allowlist. The fix is to always enforce a specific algorithm (such as HS256 or RS256) in your server-side verification code and reject tokens that specify none or any unexpected algorithm.
How long should a JWT access token be valid?
JWT access tokens should have a short expiry of 5 to 15 minutes. Short-lived tokens limit the damage if a token is stolen. Pair them with longer-lived refresh tokens (hours to days) that are stored securely and can be revoked on the server side. For highly sensitive operations, consider tokens that expire in 1 to 5 minutes.
Should I store JWTs in localStorage or cookies?
Store JWTs in HttpOnly, Secure, SameSite cookies. localStorage is accessible to any JavaScript on the page, making it a target for XSS attacks. HttpOnly cookies cannot be read by JavaScript, the Secure flag ensures HTTPS-only transmission, and SameSite provides CSRF protection. If you must use localStorage, enforce a strict Content Security Policy and audit all third-party scripts.
Is it safe to decode a JWT in the browser?
Yes. Decoding a JWT reveals the base64url-encoded claims but does not compromise security because the payload was never encrypted to begin with. Client-side tools like NexTool JWT Decoder process tokens entirely in your browser without sending data to a server. However, never trust decoded claims on the client for authorization -- always verify the signature on the server.
What is the difference between HS256 and RS256?
HS256 uses a single shared secret for both signing and verification. It is simpler and faster but requires the secret to be on every service that verifies tokens. RS256 uses an asymmetric key pair: a private key for signing and a public key for verification. RS256 is more secure in distributed systems because only the auth server needs the private key. Use HS256 for single-service architectures and RS256 for microservices or when third parties verify your tokens.
Explore 150+ Free Developer Tools
JWT tools are just the start. NexTool has free tools for JSON, regex, encoding, hashing, and much more.
Browse All Free Tools