What is JWT? A Developer's Guide to JSON Web Tokens
Learn how JSON Web Tokens work, their three-part structure, common claims, security best practices, and when to use JWTs vs sessions for authentication.
Try our free JWT Decoder
Decode any JWT token instantly -- view header, payload, and expiry status.
What is a JSON Web Token?
A JSON Web Token (JWT, pronounced "jot") is an open standard (RFC 7519) for securely transmitting information between parties as a compact, URL-safe JSON object. JWTs are widely used for authentication and authorization in modern web applications, APIs, and microservices.
When a user logs in, the server creates a JWT containing the user's identity and permissions, signs it with a secret key, and sends it back to the client. The client then includes this token in subsequent requests (typically in the Authorizationheader). The server verifies the signature to confirm the token is genuine and hasn't been tampered with.
The key advantage of JWTs is that they are stateless -- the server does not need to store session data in a database or memory. All the information needed to authenticate the user is contained within the token itself. You can inspect any JWT using our JWT Decoder to see the header, payload, and expiration status.
The Three Parts of a JWT
A JWT consists of three parts separated by dots: header.payload.signature. Each part is Base64URL-encoded.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header
The header typically contains two fields: the signing algorithm (alg) and the token type (typ):
{
"alg": "HS256",
"typ": "JWT"
}Common algorithms include HS256 (HMAC with SHA-256, symmetric key), RS256 (RSA with SHA-256, asymmetric key pair), and ES256 (ECDSA with SHA-256). RS256 is preferred for distributed systems because only the auth server needs the private key -- other services verify using the public key.
2. Payload
The payload contains the claims -- statements about the user and additional metadata:
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}The payload is only Base64URL-encoded, not encrypted. Anyone with the token can decode and read it. Never put sensitive data like passwords or credit card numbers in the payload.
3. Signature
The signature is created by combining the encoded header and payload, then signing them with the secret key:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
The signature ensures integrity -- if anyone modifies the header or payload, the signature will no longer match, and the server will reject the token. You can verify this yourself by decoding a token with our JWT Decoder and checking its structure.
Common JWT Claims Explained
JWT defines a set of registered claims -- standardized fields with specific meanings. Here are the most important ones:
| Claim | Full Name | Description |
|---|---|---|
iss | Issuer | Who issued the token (e.g., "https://auth.example.com") |
sub | Subject | Who the token refers to (usually a user ID) |
aud | Audience | Who the token is intended for (e.g., "https://api.example.com") |
exp | Expiration Time | Unix timestamp when the token expires |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Token is not valid before this time |
jti | JWT ID | Unique identifier for the token (prevents replay attacks) |
Here is how to create a JWT with these claims in JavaScript using the popular jsonwebtoken library:
import jwt from "jsonwebtoken";
const token = jwt.sign(
{
sub: "user_123",
name: "Jane Smith",
role: "editor",
iss: "https://auth.myapp.com",
aud: "https://api.myapp.com",
},
process.env.JWT_SECRET,
{ expiresIn: "1h" } // sets exp automatically
);
// Verify and decode
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // "user_123"
console.log(decoded.role); // "editor"
} catch (err) {
console.error("Invalid or expired token");
}JWT vs Session-Based Authentication
Both JWTs and server-side sessions are valid approaches to authentication. The right choice depends on your architecture:
| Aspect | JWT | Server Sessions |
|---|---|---|
| Storage | Client-side (token contains all data) | Server-side (database or in-memory store) |
| Scalability | Excellent -- no shared state between servers | Requires shared session store (Redis, DB) across servers |
| Revocation | Difficult -- token valid until expiry (needs blocklist) | Easy -- delete session from server |
| Size | Larger -- payload sent with every request | Smaller -- only session ID sent |
| Cross-domain | Easy -- send token in Authorization header | Difficult -- cookies are domain-scoped |
| Best for | APIs, microservices, mobile apps, SPAs | Traditional server-rendered apps, apps needing instant revocation |
Many production systems use a hybrid approach: JWTs for API authentication between services, and server-side sessions for user-facing web applications where instant revocation (e.g., logout, account ban) is important.
JWT Security Best Practices
- Use short expiration times -- Set access tokens to expire in 15 minutes to 1 hour. Short-lived tokens limit the damage window if a token is stolen. Use refresh tokens (stored securely) to issue new access tokens.
- Store tokens in httpOnly cookies -- HttpOnly cookies cannot be accessed by JavaScript, which protects against XSS attacks. Combine with the
SecureandSameSite=Strictflags. - Avoid localStorage for tokens -- Any JavaScript on the page can read localStorage, including injected scripts from XSS vulnerabilities. This is the most common JWT security mistake in SPAs.
- Always verify the signature -- Never trust a JWT without verifying its signature. Check the algorithm in the header matches what you expect -- do not accept
alg: "none". - Validate all claims -- Check
exp,iss, andaudon every request. An expired or misrouted token should be rejected immediately. - Use strong secrets -- For HMAC algorithms (HS256), use a secret that is at least 256 bits of entropy. For RSA (RS256), use 2048-bit keys minimum. Generate secrets with our Hash Generator.
- Implement token rotation -- Rotate refresh tokens on each use. If a refresh token is used twice, it indicates a potential theft -- invalidate all tokens for that user.
Common JWT Mistakes
These are the most frequent security and implementation errors developers make with JWTs:
1. Storing Sensitive Data in the Payload
{"password": "secret123", "ssn": "123-45-6789"}{"sub": "user_123", "role": "admin"}The payload is only Base64-encoded, not encrypted. Anyone can decode it. Only store identifiers and non-sensitive metadata.
2. Not Validating the Algorithm
// Accepting any algorithm
jwt.verify(token, secret)// Specifying allowed algorithms
jwt.verify(token, secret, { algorithms: ["HS256"] })Attackers can change the algorithm to 'none' or switch from RS256 to HS256 (using the public key as the HMAC secret). Always specify the expected algorithm.
3. Using Long-Lived Access Tokens
{ expiresIn: "30d" } // 30-day access token{ expiresIn: "15m" } // 15-minute access token + refresh tokenLong-lived access tokens give attackers a wide window to exploit stolen tokens. Use short-lived access tokens with a refresh token pattern.
4. No Token Revocation Strategy
// No way to invalidate a token before expiry// Maintain a blocklist of revoked token IDs (jti)JWTs cannot be revoked by default. For logout and security events, maintain a blocklist of revoked token IDs, or use short expiry times so compromised tokens expire quickly.
Here is a complete example of the refresh token pattern in an Express.js API:
import jwt from "jsonwebtoken";
// Login endpoint — issues both tokens
app.post("/login", async (req, res) => {
const user = await authenticateUser(req.body);
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.ACCESS_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: user.id },
process.env.REFRESH_SECRET,
{ expiresIn: "7d" }
);
// Store refresh token in httpOnly cookie
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
res.json({ accessToken });
});
// Refresh endpoint — issues new access token
app.post("/refresh", (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) return res.status(401).json({ error: "No refresh token" });
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET, {
algorithms: ["HS256"],
});
const newAccessToken = jwt.sign(
{ sub: payload.sub, role: payload.role },
process.env.ACCESS_SECRET,
{ expiresIn: "15m" }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(403).json({ error: "Invalid refresh token" });
}
});Frequently Asked Questions
Can anyone read a JWT token?
How do I check if a JWT is expired?
Should I store JWT in localStorage or cookies?
What is the difference between access tokens and refresh tokens?
Decode Your JWT Tokens Now
Paste any JWT token to instantly view the header, payload, claims, and expiration status. Free, private, runs in your browser.
Open JWT Decoder