JWT Tokens: How They Work, Security Best Practices, and Common Vulnerabilities
A comprehensive developer guide to JSON Web Tokens — understanding the anatomy, security model, common vulnerabilities, and best practices for using JWTs in production.
JSON Web Tokens (JWTs) are one of the most widely used authentication mechanisms in modern web applications. They power login systems, API authorization, single sign-on, and server-to-server communication. Despite their popularity, JWTs are frequently misunderstood, and misconfigurations regularly lead to security vulnerabilities. This guide breaks down how JWTs work, what can go wrong, and how to use them correctly.
What Is a JWT?
A JWT is a compact, URL-safe token that represents a set of claims (key-value pairs) encoded as a JSON object. The token is digitally signed so that the receiving party can verify it was issued by a trusted source and has not been tampered with. JWTs are defined in RFC 7519.
A typical JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The Anatomy of a JWT
Every JWT consists of three parts, separated by dots:
1. Header
The header specifies the token type and the signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
algindicates the algorithm used to sign the token. Common values includeHS256(HMAC-SHA256, symmetric),RS256(RSA-SHA256, asymmetric), andES256(ECDSA-SHA256, asymmetric).typis alwaysJWT.
2. Payload
The payload contains the claims — the data the token carries. There are three types of claims:
Registered claims (defined by the JWT specification):
iss(issuer) — who created the tokensub(subject) — the user or entity the token representsaud(audience) — who the token is intended forexp(expiration) — Unix timestamp after which the token is invalidiat(issued at) — when the token was creatednbf(not before) — Unix timestamp before which the token is not validjti(JWT ID) — unique identifier for the token
Public claims — defined by the application, but should use collision-resistant names (URIs or registered names from the IANA JSON Web Token Claims Registry).
Private claims — custom data agreed upon between the issuing and consuming parties:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1516239022,
"exp": 1516325422
}
3. Signature
The signature is computed by taking the Base64URL-encoded header, the Base64URL-encoded payload, concatenating them with a dot, and signing the result with the specified algorithm and a secret key:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
The signature allows the recipient to verify that the token was created by a party that possesses the secret key and that the contents have not been modified.
How JWT Authentication Works in Practice
The typical JWT authentication flow works like this:
- Login: The user submits credentials (username/password) to the authentication server.
- Token issuance: The server validates the credentials and generates a JWT containing the user's identity and permissions as claims. The server signs the token with a secret key and returns it to the client.
- Token storage: The client stores the JWT — typically in memory, localStorage, or a cookie.
- Authenticated requests: For subsequent API calls, the client includes the JWT in the
Authorizationheader:Authorization: Bearer <token>. - Token verification: The API server receives the request, extracts the JWT, verifies the signature, checks the expiration, and extracts the claims to determine the user's identity and permissions.
- Token refresh: When the access token expires, the client may use a refresh token to obtain a new access token without requiring the user to log in again.
Security Best Practices
1. Always Verify the Signature
Never trust a JWT without verifying its signature. A common vulnerability is decoding the token (which is trivial — just Base64-decode the payload) and using the claims without checking that the signature is valid. An attacker can craft any payload they want; only the signature prevents tampering.
2. Use Strong Secret Keys
For symmetric algorithms (HS256, HS384, HS512), the secret key is the only thing protecting the token. Use a key that is at least 256 bits (32 bytes) of cryptographically random data. Never use human-readable passwords, dictionary words, or short keys.
3. Prefer Asymmetric Algorithms in Distributed Systems
In systems where multiple services need to verify tokens but only the auth server should be able to create them, use asymmetric algorithms (RS256, ES256). The auth server signs with a private key; other services verify with the corresponding public key. This way, a compromised service cannot forge tokens.
4. Set Short Expiration Times
Keep access token lifetimes short — 15 minutes is a common choice. This limits the damage window if a token is stolen. Pair short-lived access tokens with longer-lived refresh tokens that can be revoked server-side.
5. Validate All Claims
Always check exp, iss, and aud claims. A token that was issued by a different service, intended for a different audience, or has expired should be rejected. Skipping these checks is a common source of authorization bypass vulnerabilities.
6. Do Not Store Sensitive Data in JWTs
JWTs are signed but not encrypted. Anyone who intercepts the token can read its contents. Do not put passwords, session secrets, personal health information, or financial data in JWT claims. If you need to include sensitive data, use JWE (JSON Web Encryption) to encrypt the token.
7. Use HTTPS Exclusively
JWTs transmitted over plain HTTP can be intercepted by any network observer. Always use HTTPS for any endpoint that sends or receives JWTs. This includes the login endpoint, token refresh endpoints, and all API endpoints that accept bearer tokens.
8. Implement Token Revocation
JWTs are stateless by design — the server does not keep a record of issued tokens. This makes revocation difficult. Common approaches include:
- Short expiration times with refresh token rotation
- Token blacklists (a server-side set of revoked token IDs)
- Token versioning (incrementing a version number in the user record and rejecting tokens with an older version)
Common JWT Vulnerabilities
The "none" Algorithm Attack
Some JWT libraries allow the alg field to be set to "none", indicating that the token has no signature. An attacker can take a valid token, modify the payload, set alg to "none", remove the signature, and submit the modified token. If the library trusts the alg header and skips verification when it sees "none", the forged token is accepted.
Mitigation: Always specify the expected algorithm on the verification side. Do not let the token's alg header dictate which algorithm your verification code uses.
Algorithm Confusion (Key Confusion)
When a server supports both symmetric and asymmetric algorithms, an attacker can take a token signed with RS256 (asymmetric), change the alg header to HS256 (symmetric), and sign it using the server's public key (which is publicly available) as the HMAC secret. If the server reads the alg header and uses HMAC verification with the public key, the forged token passes validation.
Mitigation: Specify the expected algorithm explicitly in your verification configuration. Do not derive the algorithm from the token header.
Token Sidejacking
If a JWT is stolen (via XSS, network sniffing, or log exposure), the attacker can use it as if they were the legitimate user until it expires. Without additional binding mechanisms, there is no way to distinguish a stolen token from a legitimate one.
Mitigation: Bind tokens to client characteristics (IP address, user agent fingerprint) where feasible, use short expiration times, and implement token revocation.
JWT Cracking
If the secret key is weak, an attacker can brute-force it offline by testing candidate keys against a known token and signature. Tools like hashcat and john support JWT cracking.
Mitigation: Use strong, randomly generated secret keys (256 bits minimum).
Key Injection
An attacker embeds a malicious key in the JWT header (using the jwk field) and signs the token with that key. If the server trusts the embedded key, the forged token is accepted.
Mitigation: Never use keys from the token header for verification. Always use your own trusted key or key set.
JWT vs. Session-Based Authentication
JWTs are not universally better than traditional session-based auth. Each approach has trade-offs:
| Aspect | JWT | Session | |---|---|---| | State | Stateless (server stores nothing) | Stateful (server stores session data) | | Scalability | Easier horizontal scaling | Requires shared session store (Redis) | | Revocation | Difficult without server state | Easy (delete session) | | Size | Larger (token in every request) | Smaller (just session ID cookie) | | Cross-domain | Works across domains | Complicated by cookie restrictions |
For most applications, a hybrid approach works best: use short-lived JWTs for API authorization and session cookies (with server-side session storage) for the primary web application.
Debugging JWTs
When developing with JWTs, you will frequently need to inspect token contents. Tools like Toolverse's JWT Decoder let you paste a token and immediately see the decoded header and payload, including human-readable timestamps for exp and iat claims. This is invaluable during development and debugging.
Remember: decoding a token (reading its contents) is different from verifying a token (checking its signature). Decoding tells you what the token claims; verification tells you whether those claims are trustworthy.
Conclusion
JWTs are a powerful authentication mechanism when used correctly. The key principles are: always verify signatures, use strong keys, set short expiration times, validate all claims, transmit over HTTPS, and never store sensitive data in the payload. Understanding the common vulnerabilities — especially the none algorithm attack and algorithm confusion — is essential for building secure systems. Use JWTs where they fit (stateless API auth, cross-domain SSO), but do not hesitate to use traditional sessions where they are more appropriate.