JSON Web Tokens (JWTs) are everywhere in modern web authentication. They show up in OAuth flows, API authentication, single sign-on systems, and session management. Despite their ubiquity, JWTs are frequently misunderstood and misused, leading to security vulnerabilities that could have been avoided.
This guide breaks down how JWTs work, what each part means, and the security mistakes you should watch out for.
What Is a JWT?
A JWT is a compact, URL-safe string that represents claims (pieces of information) about a subject. It is typically used to prove that the bearer of the token has been authenticated and is authorized to access certain resources.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
That is three Base64URL-encoded strings separated by dots. Each part has a specific purpose.
The Three Parts
1. Header
The first segment is the header, which specifies the token type and the signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
Common algorithms include:
- HS256 — HMAC with SHA-256 (symmetric: same secret for signing and verification)
- RS256 — RSA with SHA-256 (asymmetric: private key signs, public key verifies)
- ES256 — ECDSA with SHA-256 (asymmetric, smaller keys than RSA)
The algorithm choice matters significantly for security — more on that below.
2. Payload
The second segment contains the claims — the actual data the token carries:
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
Claims fall into three categories:
Registered claims are standardized fields defined by the JWT spec:
iss(issuer) — who created the tokensub(subject) — who the token is aboutaud(audience) — who the token is intended forexp(expiration) — when the token expires (Unix timestamp)nbf(not before) — token is not valid before this timeiat(issued at) — when the token was createdjti(JWT ID) — a unique identifier for the token
Public claims are defined by your application but should avoid collisions with registered claims. Using namespaced names (like https://myapp.com/roles) is a good practice.
Private claims are custom claims agreed upon between parties that produce and consume the token.
3. Signature
The third segment is the signature, which ensures the token has not been tampered with:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
For HS256, the server signs with a secret key. For RS256, it signs with a private key. The verifier uses the same secret (HS256) or the corresponding public key (RS256) to validate the signature.
Critical point: The payload is encoded, not encrypted. Anyone can decode and read it. The signature only guarantees integrity — it proves the payload has not been modified, not that it is hidden. Never put sensitive information (passwords, credit card numbers, PII) in a JWT payload unless you also encrypt it (using JWE).
How JWT Authentication Works
A typical JWT-based auth flow:
- User sends credentials (username/password) to the authentication server
- Server verifies credentials and generates a JWT signed with the server’s secret or private key
- Server returns the JWT to the client
- Client stores the JWT (usually in memory or an HTTP-only cookie)
- Client includes the JWT in subsequent API requests via the
Authorizationheader:Authorization: Bearer eyJhbGciOiJIUzI1NiIs... - Server verifies the JWT signature and extracts claims to authorize the request
The server never needs to look up session data in a database — all the information it needs is in the token itself. This is what makes JWTs “stateless” and useful for distributed systems.
Token Expiration and Refresh
JWTs should always have an expiration (exp claim). A token without expiration is valid forever if the signing key is not rotated — a serious security concern.
Common expiration strategies:
- Short-lived access tokens (5-15 minutes) + refresh tokens (days/weeks)
- The access token is used for API requests
- When it expires, the client uses the refresh token to get a new access token
- Refresh tokens are stored securely (HTTP-only cookie) and can be revoked server-side
{
"sub": "user123",
"exp": 1712000000,
"iat": 1711999100,
"type": "access"
}
Setting exp to 15 minutes from iat limits the damage window if a token is compromised.
Common Security Mistakes
1. The alg: none Attack
The JWT spec allows an “none” algorithm, meaning no signature. Early JWT libraries accepted tokens with "alg": "none" and skipped verification entirely. An attacker could forge any token by:
{"alg": "none", "typ": "JWT"}
Fix: Always validate the algorithm. Reject none. Most modern libraries have patched this, but verify your library’s configuration.
2. Algorithm Confusion
If a server expects RS256 (asymmetric) but an attacker sends a token with HS256 (symmetric) and signs it with the server’s public key (which is public), some libraries will accept it — they treat the public key as the HMAC secret.
Fix: Explicitly configure the expected algorithm on the verification side. Never let the token’s header dictate which algorithm to use.
3. Weak Signing Secrets
Using "secret" or "password123" as your HMAC secret is effectively no security. Attackers can brute-force weak secrets using tools like jwt_tool or hashcat.
Fix: Use a cryptographically random secret of at least 256 bits. For HS256, that is 32 random bytes. Better yet, use asymmetric algorithms (RS256, ES256) where the private key never needs to leave the server.
4. Storing Tokens in localStorage
localStorage is accessible to any JavaScript running on the page, making it vulnerable to XSS attacks. If an attacker injects a script, they can steal the token.
Fix: Store tokens in HTTP-only, Secure, SameSite cookies. If you must use JavaScript-accessible storage, ensure rigorous XSS prevention.
5. Not Validating Claims
Verifying the signature is not enough. You must also validate:
exp— reject expired tokensiss— verify the token came from your auth serveraud— verify the token was intended for your servicenbf— verify the token is not being used before its start time
Skipping claim validation means accepting tokens from other services or expired sessions.
Debugging JWTs
When troubleshooting authentication issues, you often need to inspect a JWT’s contents. Since the payload is just Base64URL-encoded JSON, you can decode it to see the claims, expiration time, and issuer without needing the signing secret.
Our JWT Decoder lets you paste a token and instantly see the decoded header, payload, and signature — with human-readable timestamps for exp, iat, and nbf claims. Everything runs in your browser, so your tokens are never sent to a server. This is particularly useful when debugging why an API is rejecting a token — you can quickly check if it is expired, issued for the wrong audience, or missing required claims.
JWTs vs. Session Tokens
JWTs are not always the right choice. Here is a quick comparison:
| Factor | JWT | Server-Side Sessions |
|---|---|---|
| Scalability | No server storage needed | Requires session store |
| Revocation | Hard (must wait for expiry) | Easy (delete from store) |
| Size | Larger (contains claims) | Small (just an ID) |
| Statelessness | Yes | No |
| Security | Careful implementation needed | Simpler security model |
For simple applications with a single server, server-side sessions are often simpler and more secure. JWTs shine in distributed architectures where multiple services need to verify identity without a shared session store.
Best Practices Summary
- Always set
expwith short lifetimes for access tokens - Use asymmetric algorithms (RS256, ES256) when possible
- Never store sensitive data in the payload
- Validate all relevant claims on every request
- Store tokens in HTTP-only cookies, not localStorage
- Explicitly configure accepted algorithms on the server
- Rotate signing keys periodically
- Implement token refresh for long-lived sessions
JWTs are a powerful tool for distributed authentication, but they require careful implementation. Understand the tradeoffs, follow security best practices, and validate rigorously. A JWT done right is elegant; a JWT done wrong is a security hole waiting to be exploited.