Understanding JWT Tokens: Structure, Security, and Best Practices
JSON Web Tokens (JWTs) are everywhere in modern web development — they power authentication in REST APIs, OAuth 2.0 flows, and single sign-on systems used by millions of applications. Yet many developers use JWTs without fully understanding their structure, limitations, or security implications.
This guide covers everything you need to know about JWTs: how they're built, what each part means, and how to avoid common pitfalls that lead to security vulnerabilities.
What is a JWT?
A JWT (pronounced "jot") is a compact, URL-safe way to represent claims between two parties. The claims are encoded as a JSON object that is digitally signed — either using a secret (HMAC) or a public/private key pair (RSA or ECDSA).
JWTs are defined in RFC 7519 and consist of three Base64URL-encoded parts separated by periods:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The three sections are:
- Header — token type and signing algorithm
- Payload — the claims (data)
- Signature — cryptographic verification
The Header
The header is a JSON object encoded in Base64URL. It typically contains two fields:
{
"alg": "RS256",
"typ": "JWT"
}
The alg field specifies the signing algorithm. Common values:
HS256— HMAC-SHA256 (symmetric, uses a shared secret)RS256— RSA-SHA256 (asymmetric, uses a private/public key pair)ES256— ECDSA with P-256 (asymmetric, smaller signatures than RSA)none— No signature (dangerous! Never accept this in production)
The Payload
The payload contains claims — statements about the user or additional metadata. Standard claims (defined in RFC 7519):
| Claim | Name | Description |
|---|---|---|
iss |
Issuer | Who issued this token |
sub |
Subject | Who the token is about (usually user ID) |
aud |
Audience | Who should accept this token |
exp |
Expiration | When the token expires (Unix timestamp) |
nbf |
Not Before | Token not valid before this time |
iat |
Issued At | When the token was issued |
jti |
JWT ID | Unique identifier for the token |
A typical payload looks like:
{
"sub": "user_abc123",
"name": "Jane Smith",
"email": "jane@example.com",
"roles": ["user", "admin"],
"iat": 1709856000,
"exp": 1709942400
}
Important: The payload is Base64URL-encoded, NOT encrypted. Anyone who has the token can read the payload. Never put sensitive data (passwords, credit card numbers, PII) in a JWT payload unless the token is also encrypted (JWE).
The Signature
The signature ensures the token hasn't been tampered with. For HMAC-SHA256 (HS256):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
For RSA (RS256), the server signs with its private key and clients verify with the public key. This allows token verification without sharing secrets — useful for microservices and third-party integrations.
How JWT Authentication Works
- User logs in with credentials
- Server validates credentials, creates JWT with user claims
- Server signs JWT and returns it to the client
- Client stores JWT (localStorage, sessionStorage, or httpOnly cookie)
- Client sends JWT in the
Authorization: Bearer <token>header - Server verifies JWT signature and extracts claims
- Server authorizes the request based on the claims
The key advantage: the server doesn't need to store session state. The token carries everything needed for authorization.
Common JWT Vulnerabilities
1. The alg: none Attack
Some early JWT libraries accepted tokens with "alg": "none" — meaning no signature required. An attacker could modify any token, set the algorithm to none, and the server would accept it.
Fix: Always whitelist acceptable algorithms. Reject tokens with alg: none.
2. Algorithm Confusion (RS256 → HS256)
If a server uses RS256 (asymmetric), an attacker might send a token with "alg": "HS256", where the "secret" is the server's public key (which is, by definition, public). Libraries that don't explicitly verify the algorithm may accept this.
Fix: Always specify the expected algorithm when verifying. Never derive it from the token header.
3. Weak Secrets
HMAC-based JWTs using short or predictable secrets can be cracked offline. An attacker with a valid token can brute-force the secret using tools like hashcat.
Fix: Use cryptographically random secrets of at least 256 bits for HMAC. Better yet, use RS256 or ES256.
4. No Expiration
Tokens without an exp claim live forever. If a token is stolen, the attacker has permanent access.
Fix: Always set short expiration times (15–60 minutes for access tokens). Use refresh tokens for longer sessions.
5. Sensitive Data in Payload
Since JWT payloads are only Base64-encoded (not encrypted), storing sensitive data exposes it to anyone who captures the token.
Fix: Only include non-sensitive identifiers (user IDs) in the payload. Fetch sensitive data server-side.
6. JWT Storage in localStorage
Storing JWTs in localStorage exposes them to XSS attacks — any JavaScript on your page can read them.
Fix: Store tokens in httpOnly, Secure cookies. These aren't accessible to JavaScript, mitigating XSS token theft.
JWT Best Practices
For Token Generation
- Use
RS256orES256for production (asymmetric — easier key rotation, no shared secrets) - Set short
exptimes: 15 minutes for access tokens - Always include
iss,aud,sub, andexpclaims - Use cryptographically random
jtito enable token revocation
For Token Verification
- Always verify the signature before trusting any claims
- Validate all claims:
exp,nbf,iss,aud - Whitelist algorithms — never trust the
algheader value blindly - Reject tokens with
alg: none - Check
expwith a small clock skew tolerance (< 60 seconds)
For Token Storage and Transmission
- Use httpOnly, Secure, SameSite=Strict cookies for web apps
- Use
Authorization: Bearer <token>header for API-to-API communication - Use HTTPS everywhere — JWTs in plaintext HTTP are trivially stolen
- Never log full JWT tokens (they're credentials)
JWT vs Session Tokens
| Feature | JWT | Session Token |
|---|---|---|
| Storage | Client-side | Server-side |
| Revocation | Hard (requires denylist) | Easy (delete session) |
| Scalability | Stateless — scales easily | Requires session store |
| Size | Larger (~200-500 bytes) | Small (~32 bytes) |
| Security | Depends on implementation | Easier to get right |
| Use case | APIs, microservices, SSO | Traditional web apps |
Decoding JWTs
You can decode any JWT to inspect its contents using our free JWT Decoder tool. The decoder splits the token into its three parts, decodes the Base64URL encoding, and displays the header and payload as formatted JSON.
For testing and development, use the JWT Debugger to edit claims and re-encode tokens, or the JWT Encoder to create signed tokens with custom claims.
Refresh Token Pattern
For persistent authentication, combine short-lived access tokens with long-lived refresh tokens:
- Access Token: 15-60 minute expiry, stored in memory or httpOnly cookie
- Refresh Token: 7-30 day expiry, stored in httpOnly cookie only, stored server-side for revocation
When the access token expires, the client sends the refresh token to get a new access token — without requiring the user to log in again. The server validates the refresh token (checking it hasn't been revoked) before issuing a new access token.
This pattern gives you:
- Short blast radius if an access token is stolen
- Ability to revoke sessions (by invalidating the refresh token)
- Persistent user sessions without security compromise
Conclusion
JWTs are a powerful tool for stateless authentication, but they're often misimplemented. The most critical rules:
- Always verify the signature with a whitelisted algorithm
- Always validate claims — especially
exp,iss, andaud - Use asymmetric algorithms (RS256/ES256) in production
- Store tokens securely — httpOnly cookies, not localStorage
- Keep tokens short-lived and use refresh tokens for persistence
Use our JWT Decoder to inspect any token, JWT Encoder to generate signed tokens, or PKCE Generator if you're implementing OAuth 2.0 authorization code flow with PKCE.