JWT Explained: How JSON Web Tokens Work and How to Use Them Securely
JSON Web Tokens (JWT) are everywhere in modern authentication — from single-page applications to mobile
APIs and microservices. But they are also consistently misused. This guide explains exactly how JWTs work,
what they protect against, where they fall short, and the critical security rules you must follow.
1. What Is a JSON Web Token?
A JWT (pronounced “jot”) is a compact, URL-safe token format defined in
RFC 7519.
It is used to transfer claims (pieces of information) between two parties in a way that can be
cryptographically verified — but not necessarily encrypted.
The most common use is stateless authentication: after a user logs in, the server issues a JWT.
The client stores the token (typically in memory or an HttpOnly cookie) and sends it with subsequent
requests. The server validates the token without needing to look up a session in a database.
Key Insight: A JWT is not a session. It is a self-contained credential. The server
trusts the token because it can verify the cryptographic signature — no database lookup needed.
2. JWT Structure: Header.Payload.Signature
A JWT consists of three Base64Url-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzExNDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header
The header declares the token type and the signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
The payload contains the claims — statements about the user and any additional metadata.
RFC 7519 defines several registered claim names:
sub — Subject (typically the user ID)
iss — Issuer (who issued the token)
aud — Audience (who the token is intended for)
exp — Expiration time (Unix timestamp)
iat — Issued at (when the token was issued)
nbf — Not before (token is invalid before this time)
jti — JWT ID (unique identifier for the token)
{
"sub": "1234567890",
"name": "Alice",
"role": "admin",
"iat": 1711400000,
"exp": 1711403600
}
Security Warning: The payload is Base64Url-encoded, not encrypted. Anyone who
intercepts the token can decode and read its contents. Never store passwords, payment details,
or other sensitive secrets in a JWT payload.
Signature
The signature ensures the token has not been tampered with. It is computed as:
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret_key
)
If any part of the token is modified (even a single character), the signature will not match and
the server must reject the token.
3. How JWT Authentication Works
The typical JWT authentication flow:
- Login: User submits credentials to
POST /auth/login
- Issue token: Server verifies credentials, generates a JWT signed with a secret key, and returns it to the client
- Store token: Client stores the JWT (memory, cookie, or secure storage)
- Authenticated request: Client sends the JWT in the
Authorization: Bearer <token> header
- Verify token: Server validates the signature, checks
exp, and trusts the claims
- Process request: Server uses the claims (e.g.
sub, role) without a database lookup
// Node.js example — issuing a JWT
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'api.myapp.com', audience: 'myapp.com' }
);
// Verifying a JWT
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'api.myapp.com',
audience: 'myapp.com'
});
console.log('User ID:', payload.sub);
} catch (err) {
// Token is invalid, expired, or tampered
res.status(401).json({ error: 'Unauthorized' });
}
4. Signing Algorithms: HS256 vs RS256 vs ES256
HS256 — HMAC-SHA256 (Symmetric)
Uses one shared secret key for both signing and verification. Fast and simple, but the same key
must be available to every service that verifies tokens. If a service is compromised, it could
also forge tokens.
Use when: A single application or monolith issues and verifies tokens.
RS256 — RSA-SHA256 (Asymmetric)
Uses a private key to sign and a public key to verify.
The public key can be freely distributed to all services that need to verify tokens,
but only the auth service holding the private key can issue new tokens.
Use when: Multiple microservices need to verify tokens but only one service should issue them.
ES256 — ECDSA-SHA256 (Asymmetric)
Similar to RS256 but uses Elliptic Curve cryptography. Produces much shorter signatures
(64 bytes vs ~256 bytes for RS256) while providing equivalent security. Preferred for
resource-constrained environments.
Recommendation: Use RS256 or ES256 for new production systems. The asymmetric
key approach is more secure and scales better in microservice architectures.
5. Critical Security Considerations
5.1 Always Verify the Algorithm
The most infamous JWT vulnerability: the “none” algorithm attack. Some early libraries
accepted {"alg":"none"} in the header, bypassing signature verification entirely.
// VULNERABLE — never do this
const payload = jwt.decode(token); // only decodes, does NOT verify
// SECURE — always verify with an explicit algorithm
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });
5.2 Always Validate exp, iss, and aud
A token with a valid signature but an expired exp must still be rejected.
Always validate iss (issuer) and aud (audience) to prevent
a token issued for one service from being used in another.
5.3 Use Short Expiry + Refresh Tokens
Long-lived JWTs are difficult to revoke if stolen. Use short-lived access tokens
(15–60 minutes) paired with a refresh token stored in an HttpOnly cookie.
The refresh token allows silently obtaining new access tokens without requiring re-login.
5.4 Store Tokens Safely
The safest option for web applications is an HttpOnly, Secure cookie —
inaccessible to JavaScript - combined with SameSite=Strict to prevent CSRF.
Avoid localStorage — it is accessible to any JavaScript on the page,
including malicious injected scripts.
5.5 Never Put Sensitive Data in the Payload
The payload is encoded, not encrypted. Store only non-sensitive identifiers (user ID, roles).
Never store passwords, credit card numbers, PII, or secrets in JWT claims.
5.6 Rotate Signing Keys
Periodically rotating signing keys limits the blast radius if a key is compromised.
Implement key versioning with a kid (key ID) header parameter so clients
can look up the correct public key.
6. JWT vs Server-Side Sessions
| Aspect |
JWT (Stateless) |
Server Sessions (Stateful) |
| Scalability | Excellent — no shared state needed | Requires sticky sessions or shared session store (Redis) |
| Revocation | Difficult — need a denylist or short expiry | Immediate — delete the session record |
| Payload size | Sent with every request — keep small | Only a session ID sent (small cookie) |
| Data freshness | Stale until expiry — role changes not instant | Always fresh — data read from store per request |
| Best for | APIs, microservices, mobile clients | Traditional web apps with server-rendered pages |
JWTs are not universally superior. For applications that need instant token revocation
(financial systems, healthcare, admin panels), traditional sessions with a shared store
are often a better choice despite the added infrastructure complexity.
7. Decoding JWTs in Practice
You can decode any JWT to inspect its contents using our
JWT Decoder/Encoder tool. Remember: decoding is not the same as
verifying. Always use a library with the proper secret key to verify signatures in production code.
// Quick base64url decode in the browser console
const [header, payload] = 'eyJ...token...'.split('.');
console.log(JSON.parse(atob(header.replace(/-/g,'+').replace(/_/g,'/'))));
console.log(JSON.parse(atob(payload.replace(/-/g,'+').replace(/_/g,'/'))));
Frequently Asked Questions
Are JWTs encrypted?
Not by default. A standard JWT (JWS) is only signed, not encrypted. Anyone
who intercepts the token can Base64Url-decode and read the payload. JWE (JSON Web Encryption)
provides actual encryption but is far less common. Never put sensitive secrets in a JWT payload.
What happens if a JWT is stolen?
The stolen token is valid until it expires — there is no server-side state to invalidate.
Use short expiry windows (15–60 minutes), refresh tokens, and HttpOnly cookies to
minimize the impact of token theft.
Should I store JWTs in localStorage or cookies?
HttpOnly cookies are safer. They are inaccessible to JavaScript, preventing
XSS token theft. Also set Secure and SameSite=Strict.
localStorage is convenient but exposes tokens to any JavaScript on the page.
What is the “none” algorithm vulnerability?
Some early JWT libraries accepted "alg": "none", bypassing signature verification
entirely. Always explicitly specify allowed algorithms in your verifier and reject tokens with
"none" as the algorithm.
Can I use JWT without a library?
You can decode the header and payload without a library. But never implement
signature verification yourself — use a well-tested cryptography library. Subtle
implementation errors in signature verification are a common security vulnerability.