What Is a JSON Web Token (JWT)?
A JSON Web Token, universally abbreviated as JWT and pronounced "jot," is a compact, URL-safe token format defined in RFC 7519. It enables you to transmit information between two parties as a digitally signed JSON object. The signature guarantees the data has not been tampered with in transit, making JWTs the backbone of stateless authentication in modern web applications.
Unlike session-based authentication — where the server stores session data and the client carries only a session ID — JWT-based authentication is stateless. The token itself contains all the information the server needs to identify and authorize the user. No database lookup is required on each request, which makes JWTs extremely efficient at scale.
In practice, the flow works like this: the user logs in with their credentials, the server validates them and returns a signed JWT, and the client includes that token in the Authorization header of every subsequent request. The server verifies the signature, reads the claims, and processes the request — all without touching a database.
Why it matters: JWTs are used by virtually every modern API, single-page application, and mobile app for authentication. Understanding how they work is not optional for developers — it is foundational knowledge.
The Three-Part Structure of a JWT
Every JWT consists of exactly three parts, separated by dots (.): the Header, the Payload, and the Signature. Each part is Base64URL-encoded, which means it uses only URL-safe characters and can be safely transmitted in URLs, HTTP headers, and HTML forms.
Here is a real JWT you can decode right now:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Part 1 (Header): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Part 2 (Payload): eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9
Part 3 (Signature): SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
🔑 Decode This JWT Instantly → JWT Decoder Tool
Part 1: The Header
The header is a JSON object that describes how the token is signed. It typically contains two fields: alg (the signing algorithm) and typ (the token type, almost always "JWT").
{
"alg": "HS256",
"typ": "JWT"
}
The most common algorithms are:
- HS256 (HMAC + SHA-256) — Symmetric. Same secret key signs and verifies. Simple but requires sharing the secret with every service that needs to verify tokens.
- RS256 (RSA + SHA-256) — Asymmetric. Private key signs, public key verifies. Preferred in distributed systems where multiple services need to verify tokens but only the auth server should create them.
- ES256 (ECDSA + SHA-256) — Asymmetric with elliptic curves. Smaller keys, faster operations, same security level as RS256. Growing in popularity.
- EdDSA (Ed25519) — Modern, fast, and secure. Gaining adoption quickly in 2026. Excellent performance with high security margins.
Part 2: The Payload
The payload contains the claims — statements about the user and additional metadata. Claims are the actual data your application uses for authorization and identity.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1770768000,
"role": "admin"
}
There are three categories of claims:
Registered claims are standardized names defined in the JWT specification. They are not mandatory but are strongly recommended for interoperability:
iss(Issuer) — Who created and signed the token. For example,"iss": "https://auth.myapp.com". Your server should verify this matches the expected issuer to reject tokens from rogue sources.sub(Subject) — The entity the token describes, typically a user ID. For example,"sub": "user_4f8a2b". This is the primary identifier your application uses to look up the user.aud(Audience) — The intended recipient of the token. For example,"aud": "https://api.myapp.com". If your API receives a token with a different audience, it should reject it.exp(Expiration Time) — A Unix timestamp after which the token is no longer valid. This is the most critical security claim — tokens without expiration are a major vulnerability.iat(Issued At) — When the token was created. Useful for calculating token age and detecting stale tokens.nbf(Not Before) — The token should not be accepted before this timestamp. Useful for tokens that should activate in the future.jti(JWT ID) — A unique identifier for the token. Used to prevent token replay attacks by maintaining a revocation list.
Public claims are custom names registered in the IANA JSON Web Token Claims registry to avoid naming collisions between different systems.
Private claims are entirely custom. These are the claims you define for your specific application — things like role, permissions, team_id, or plan. Just remember that anyone can decode the payload, so never put sensitive data here (passwords, credit card numbers, etc.).
Important: The payload is Base64URL-encoded, not encrypted. Anyone with access to the token can decode and read the claims. The signature only guarantees integrity (the data has not been modified), not confidentiality. If you need to hide the payload contents, use JWE (JSON Web Encryption) instead of JWS (JSON Web Signature).
Part 3: The Signature
The signature is what makes JWTs trustworthy. It is created by taking the encoded header and payload, concatenating them with a dot, and then signing them with the specified algorithm and a secret key (or private key for asymmetric algorithms).
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
// With the example token:
HMACSHA256(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + "." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9",
"your-256-bit-secret"
)
When the server receives a JWT, it recomputes the signature using the same algorithm and secret. If the recomputed signature matches the one in the token, the token is valid. If even a single character in the header or payload has been modified, the signature will not match and the token will be rejected.
Want to see this process in action? Use our Hash Generator to understand how HMAC-SHA256 works, then paste a complete token into the JWT Decoder to see the three parts broken down instantly.
Base64URL Encoding in JWTs
JWTs use Base64URL encoding (not standard Base64) for the header and payload. The difference is subtle but important: Base64URL replaces + with - and / with _, and it omits the trailing = padding characters. This makes the output safe for URLs, query parameters, and HTTP headers without any additional percent-encoding.
// Split the JWT into its three parts
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const [headerB64, payloadB64, signature] = token.split(".");
// Base64URL decode function
function base64UrlDecode(str) {
// Replace Base64URL chars with standard Base64 chars
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
// Add padding if needed
while (base64.length % 4 !== 0) base64 += "=";
return JSON.parse(atob(base64));
}
const header = base64UrlDecode(headerB64);
const payload = base64UrlDecode(payloadB64);
console.log("Header:", header);
// { alg: "HS256", typ: "JWT" }
console.log("Payload:", payload);
// { sub: "1234567890", name: "John Doe", iat: 1516239022, exp: 1770768000, role: "admin" }
// Check if token is expired
const isExpired = payload.exp * 1000 < Date.now();
console.log("Expired:", isExpired);
If you work with Base64 encoding regularly, our Base64 Encoder/Decoder lets you encode and decode strings instantly. This is especially handy when debugging tokens where the payload appears garbled or you suspect encoding issues.
🔄 Base64 Encoder/Decoder → Free ToolHow to Debug JWT Tokens in Practice
Debugging JWT issues is one of the most common tasks in web development. When authentication fails, the token is usually the first suspect. Here is a systematic approach to diagnosing JWT problems.
Step 1: Decode the Token
Start by decoding the token to inspect its contents. You can do this in your browser console, in your terminal, or with an online tool. Our JWT Decoder is purpose-built for this — paste the token and instantly see the header, payload, and expiration status.
Step 2: Check Expiration
The most common JWT issue is an expired token. Look at the exp claim and compare it to the current Unix timestamp. Remember that exp is in seconds (not milliseconds) since the Unix epoch (January 1, 1970).
# Decode the payload (middle part) of a JWT
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9" | \
tr '_-' '/+' | base64 -d 2>/dev/null | python3 -m json.tool
# Check if a timestamp is expired
python3 -c "
import time
exp = 1770768000
now = int(time.time())
print(f'Expires: {time.ctime(exp)}')
print(f'Now: {time.ctime(now)}')
print(f'Expired: {now > exp}')
print(f'Remaining: {(exp - now) // 3600} hours')
"
Step 3: Verify the Issuer and Audience
If the token is not expired, check that iss and aud match what your server expects. A token issued by auth.staging.myapp.com will be rejected by a server that only accepts tokens from auth.myapp.com. This is a frequent source of bugs when switching between environments.
Step 4: Verify the Signature
If the claims look correct, the issue might be a signature mismatch. This happens when the signing secret has been rotated, the token was created by a different service, or (in asymmetric setups) the wrong public key is being used for verification. Our JWT Decoder highlights signature verification status so you can quickly identify this class of issue.
JWT Security Best Practices
JWTs are secure when implemented correctly. The problem is that "correctly" involves several non-obvious decisions. Here are the practices that matter most.
Where to Store Tokens
This is the single most debated topic in JWT security, and the answer is unambiguous: use HttpOnly cookies.
- HttpOnly + Secure + SameSite cookies (Recommended) — JavaScript cannot access the token, which eliminates XSS as an attack vector. The
Secureflag ensures the cookie is only sent over HTTPS.SameSite=StrictorSameSite=Laxprevents CSRF. This is the gold standard. - localStorage (Common but risky) — Convenient for SPAs but vulnerable to XSS attacks. Any JavaScript running on the page — including compromised third-party scripts — can steal the token. If you must use localStorage, keep token lifetimes under 15 minutes.
- sessionStorage (Slightly better) — Cleared when the tab closes, which limits the exposure window. Still vulnerable to XSS during the active session.
- In-memory variable (Most secure for SPAs) — Store the access token in a JavaScript variable. It disappears on refresh, so pair it with a refresh token stored in an HttpOnly cookie. This is the pattern recommended by OWASP for SPAs.
Token Expiration Strategy
Short-lived access tokens combined with longer-lived refresh tokens is the standard pattern:
- Access token: 5 to 15 minutes. This is the token sent with every API request. A short lifetime limits the damage if it is stolen.
- Refresh token: 7 to 30 days. Stored securely (HttpOnly cookie), used only to obtain a new access token when the current one expires. Should be single-use — after each refresh, issue a new refresh token and invalidate the old one (refresh token rotation).
async function fetchWithAuth(url, options = {}) {
let accessToken = getAccessToken(); // From memory
// Try the request with the current access token
let response = await fetch(url, {
...options,
headers: {
...options.headers,
"Authorization": `Bearer ${accessToken}`
}
});
// If 401 Unauthorized, try refreshing the token
if (response.status === 401) {
const refreshResponse = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include" // Sends the HttpOnly refresh token cookie
});
if (refreshResponse.ok) {
const { access_token } = await refreshResponse.json();
setAccessToken(access_token); // Store in memory
// Retry the original request with the new token
response = await fetch(url, {
...options,
headers: {
...options.headers,
"Authorization": `Bearer ${access_token}`
}
});
} else {
// Refresh failed — redirect to login
window.location.href = "/login";
}
}
return response;
}
Additional Best Practices
- Always validate all registered claims — Check
exp,iss,aud, andnbfon every request. Do not rely on the token being valid just because the signature checks out. - Use strong secrets — For HS256, your secret must be at least 256 bits (32 bytes) of cryptographically random data. A password like "my-secret" is trivially brute-forced. Use
openssl rand -base64 32to generate a proper secret. - Keep payloads small — Every claim adds bytes to every request. Do not store entire user profiles in the token. Include only what is needed for authorization (user ID, role, permissions).
- Implement token revocation — For critical actions (password change, account compromise), you need the ability to invalidate tokens before they expire. Options include a blocklist, a token version counter, or short-lived tokens with refresh token revocation.
- Use asymmetric algorithms in production — RS256 or ES256 separates the signing authority (private key) from verification (public key). This is essential when multiple services need to verify tokens.
Common JWT Vulnerabilities
Understanding how JWTs can be attacked is just as important as knowing how to use them correctly. Here are the vulnerabilities that have caused real-world breaches.
1. The "none" Algorithm Attack
This is the most infamous JWT vulnerability. The JWT specification technically allows "alg": "none", which means "no signature." If a server's JWT library naively trusts the algorithm specified in the header, an attacker can:
- Decode the payload of a valid token
- Modify the claims (e.g., change
"role": "user"to"role": "admin") - Set the header algorithm to
"alg": "none" - Remove the signature entirely
- Send the forged token, which the server accepts as valid
// Original token (signed with HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIifQ.validSignature
// Attacker's forged token (no signature):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0.
// Decoded header: { "alg": "none", "typ": "JWT" }
// Decoded payload: { "sub": "1234567890", "role": "admin" } <-- elevated to admin!
Prevention: Always explicitly whitelist the algorithms your server accepts. Never let the token header dictate which algorithm to use for verification.
import jwt from "jsonwebtoken";
// WRONG: Trusts the algorithm from the token header
const decoded = jwt.verify(token, secret);
// CORRECT: Explicitly specifies allowed algorithms
const decoded = jwt.verify(token, secret, {
algorithms: ["HS256"], // Only allow HS256
issuer: "https://auth.myapp.com",
audience: "https://api.myapp.com"
});
2. Token Exposure in URLs
Passing JWTs as URL query parameters is a common anti-pattern:
https://api.example.com/data?token=eyJhbGciOiJIUzI1NiIs...
This is dangerous because URLs are logged in server access logs, browser history, proxy logs, and analytics tools. Anyone with access to these logs gains access to valid authentication tokens. Additionally, the Referer header will leak the full URL (including the token) to any external resource loaded on the page. Always send tokens in the Authorization header or in HttpOnly cookies.
3. Long-Lived Tokens Without Revocation
A JWT with a 30-day expiration and no revocation mechanism is essentially an irrevocable 30-day key to your system. If the token is stolen, the attacker has a full month of access. The solution is to use short-lived access tokens (5-15 minutes) paired with refresh tokens that can be revoked individually.
4. Algorithm Confusion (RS256 vs HS256)
In systems using RS256, the server verifies tokens with the public key. An attacker can change the header to "alg": "HS256" and sign the token using the public key as the HMAC secret. If the server does not enforce the expected algorithm, it will verify the forged token using the public key as an HMAC secret, and the verification will succeed. This is another reason to always whitelist algorithms explicitly.
5. Sensitive Data in the Payload
Because the JWT payload is only encoded (not encrypted), any data in the payload is readable by anyone who intercepts the token. Never include passwords, social security numbers, credit card numbers, or other sensitive data in JWT claims. If you need the payload to be confidential, use JWE (JSON Web Encryption).
Security tip: Run your JWT implementation through a checklist: Are algorithms explicitly whitelisted? Are tokens short-lived? Is there a revocation mechanism? Are tokens stored in HttpOnly cookies? Is the payload free of sensitive data? If the answer to any of these is "no," you have work to do.
JWT vs. Session Tokens vs. OAuth
JWTs are not always the right choice. Here is when to use each approach:
- JWT (Stateless tokens): Best for microservices, distributed systems, and APIs serving mobile and SPA clients. The server does not need to store session state, which simplifies horizontal scaling. The tradeoff is that revocation requires extra infrastructure.
- Session tokens (Stateful): Best for traditional server-rendered applications. The server stores the session data and the client carries a simple session ID cookie. Revocation is trivial (delete the session record). The tradeoff is that you need a session store (Redis, database) that all servers can access.
- OAuth 2.0: An authorization framework, not an authentication mechanism. OAuth defines how third-party applications can obtain limited access to a user's account. JWTs are often used as the token format within OAuth flows. OAuth is the right choice when you need to grant third-party access to user resources.
For most modern APIs and SPAs, JWTs with refresh token rotation strike the best balance of performance, scalability, and security.
Practical JWT Implementation
Here is a complete, production-ready JWT authentication flow in Node.js with Express. This example implements all the best practices discussed above: short-lived access tokens, refresh token rotation, HttpOnly cookies, and explicit algorithm verification.
import jwt from "jsonwebtoken";
import crypto from "crypto";
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET; // 256-bit random key
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // Different 256-bit key
const ACCESS_EXPIRY = "15m"; // 15 minutes
const REFRESH_EXPIRY = "7d"; // 7 days
// Store for revoked refresh tokens (use Redis in production)
const revokedTokens = new Set();
// Generate token pair
function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
ACCESS_SECRET,
{
algorithm: "HS256",
expiresIn: ACCESS_EXPIRY,
issuer: "https://auth.myapp.com",
audience: "https://api.myapp.com"
}
);
const refreshToken = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() },
REFRESH_SECRET,
{ algorithm: "HS256", expiresIn: REFRESH_EXPIRY }
);
return { accessToken, refreshToken };
}
// Auth middleware — verifies access token from Authorization header
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing or malformed token" });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, ACCESS_SECRET, {
algorithms: ["HS256"], // Explicit algorithm whitelist
issuer: "https://auth.myapp.com",
audience: "https://api.myapp.com"
});
req.user = { id: payload.sub, role: payload.role };
next();
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(401).json({ error: "Token expired" });
}
return res.status(401).json({ error: "Invalid token" });
}
}
// POST /api/auth/login
app.post("/api/auth/login", async (req, res) => {
const user = await validateCredentials(req.body);
if (!user) return res.status(401).json({ error: "Invalid credentials" });
const { accessToken, refreshToken } = generateTokens(user);
// Set refresh token as HttpOnly cookie
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: "/api/auth/refresh" // Only sent to refresh endpoint
});
// Return access token in response body (stored in memory by client)
res.json({ access_token: accessToken, expires_in: 900 });
});
// POST /api/auth/refresh — Refresh token rotation
app.post("/api/auth/refresh", (req, res) => {
const oldRefreshToken = req.cookies.refresh_token;
if (!oldRefreshToken) return res.status(401).json({ error: "No refresh token" });
try {
const payload = jwt.verify(oldRefreshToken, REFRESH_SECRET, {
algorithms: ["HS256"]
});
// Check if this token has been revoked
if (revokedTokens.has(payload.jti)) {
return res.status(401).json({ error: "Token revoked" });
}
// Revoke the old refresh token (single-use)
revokedTokens.add(payload.jti);
// Issue new token pair
const user = { id: payload.sub, role: payload.role };
const { accessToken, refreshToken } = generateTokens(user);
res.cookie("refresh_token", refreshToken, {
httpOnly: true, secure: true, sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, path: "/api/auth/refresh"
});
res.json({ access_token: accessToken, expires_in: 900 });
} catch {
return res.status(401).json({ error: "Invalid refresh token" });
}
});
Decode Any JWT Instantly
Paste any JWT token and see the decoded header, payload, and expiration status in real time. No signup, no installation, completely free.
Open JWT DecoderEssential Tools for Working with JWTs
Whether you are building an authentication system, debugging a failing API call, or auditing your security posture, these tools will save you significant time:
- JWT Decoder — Paste any JWT and instantly see the decoded header, payload, expiration status, and algorithm. The fastest way to debug token issues.
- JWT Generator — Create test JWTs with custom claims, algorithms, and expiration times. Essential for building and testing auth flows without a running server.
- Base64 Encoder/Decoder — Encode and decode Base64 and Base64URL strings. Useful when you need to manually inspect or modify individual JWT parts.
- Hash Generator — Generate SHA-256, HMAC, and other hashes. Helps you understand how JWT signatures are computed and verify hash outputs.
Frequently Asked Questions
A JSON Web Token (JWT) is a compact, URL-safe token format used for securely transmitting information between parties as a JSON object. It consists of three Base64URL-encoded parts separated by dots: a Header (specifying the algorithm and token type), a Payload (containing claims like user ID, expiration time, and roles), and a Signature (a cryptographic hash that verifies the token has not been tampered with). The server creates the token upon login, the client stores it, and sends it with every subsequent request for stateless authentication.
To decode a JWT token, split it at the two dot characters to get three parts: Header, Payload, and Signature. The Header and Payload are Base64URL-encoded JSON strings — decode them using a Base64 decoder to see the JSON contents. The Signature cannot be decoded; it must be verified using the secret key or public key. You can use free online tools like NexTool's JWT Decoder to instantly decode and inspect any JWT token without writing code.
The safest place to store JWT tokens on the client side is in an HttpOnly, Secure, SameSite cookie. This prevents JavaScript from accessing the token, which protects against XSS attacks. Avoid storing JWTs in localStorage or sessionStorage because any JavaScript running on the page (including third-party scripts or injected code) can read them. If you must use localStorage, keep token lifetimes very short (under 15 minutes) and implement refresh token rotation with an HttpOnly cookie for the refresh token.
The "none" algorithm attack is a well-known JWT vulnerability where an attacker modifies the token header to set the algorithm to "none," effectively removing signature verification. If the server's JWT library accepts the "none" algorithm, it will treat the token as valid without checking any signature, allowing the attacker to forge tokens with arbitrary claims (like elevating their role to admin). To prevent this, always explicitly specify the allowed algorithms when verifying tokens and reject any token with alg set to "none."
HS256 (HMAC + SHA-256) is a symmetric algorithm that uses the same secret key for both signing and verifying tokens. It is simple and fast but requires sharing the secret with every service that needs to verify tokens. RS256 (RSA + SHA-256) is an asymmetric algorithm that uses a private key for signing and a separate public key for verification. RS256 is preferred in distributed systems and microservices architectures because only the authentication server needs the private key, while any service can verify tokens using the publicly available public key.
NexTool Pro — Unlimited Access to All Tools
Get advanced features, no usage limits, and priority access to new tools. One-time payment, lifetime access.
Get Pro — $29