JWT Tokens Explained: Structure, Security & Decoding
Understand JSON Web Tokens from the inside out. Learn the header, payload, and signature structure, common claims, signing algorithms, and security best practices.
What Is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe string that represents claims between two parties. It is the most common way to handle authentication and authorization in modern web applications.
When you log into a web app and the server gives you back a token instead of creating a server-side session, chances are that token is a JWT. Your browser sends it with every subsequent request, and the server verifies the signature without hitting a database.
Where JWTs Are Used
- Authentication. After a user logs in, the server issues a JWT. The client stores it and sends it in the
Authorizationheader on every request. - Single Sign-On (SSO). JWTs let users authenticate once and access multiple services. Identity providers like Auth0, Okta, and Keycloak issue JWTs that downstream services can verify independently.
- API authorization. Microservices pass JWTs between each other to prove that a request was made on behalf of an authenticated user with specific permissions.
- Information exchange. Because JWTs are signed, they can carry trusted data between parties without an additional verification step.
The Three Parts of a JWT
Every JWT consists of three Base64URL-encoded parts separated by dots:
header.payload.signatureHere is a real JWT (shortened for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cLet us decode each part.
Part 1: Header
The header describes the token type and the signing algorithm. Decoded from Base64URL:
{
"alg": "HS256",
"typ": "JWT"
}alg tells the verifier which algorithm to use when checking the signature. typ confirms this is a JWT (as opposed to a JWE or other token type).
Part 2: Payload (Claims)
The payload contains the claims — the actual data the token carries. Decoded:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}Claims are just key-value pairs. Some are standardized (called "registered claims"), and you can add any custom claims you need.
The payload is Base64URL-encoded, not encrypted. Anyone with the token can decode and read the payload. Never put secrets, passwords, or sensitive personal data in a JWT payload.
Part 3: Signature
The signature is computed by taking the encoded header, a dot, the encoded payload, and signing it with a secret key:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)The signature guarantees that the token has not been tampered with. If anyone changes a single character in the header or payload, the signature will not match, and the verifier will reject the token.
Try it yourself: Paste any JWT into our JWT Decoder to inspect all three parts instantly.
Common JWT Claims
The JWT specification (RFC 7519) defines a set of registered claims. None are mandatory, but they are widely adopted:
| Claim | Full Name | Description |
|---|---|---|
sub | Subject | Who the token is about (usually a user ID) |
iss | Issuer | Who issued the token (e.g., your auth server URL) |
aud | Audience | Intended recipient (e.g., your API URL) |
exp | Expiration Time | Unix timestamp after which the token is invalid |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Token is invalid before this unix timestamp |
jti | JWT ID | Unique identifier to prevent token reuse |
Custom claims are anything else you add. Common examples include role, permissions, email, and org_id. Keep custom claims minimal — every extra byte gets sent with every request.
Signing Algorithms Compared
The algorithm choice determines how the signature is created and verified. Here are the three most common options:
| Algorithm | Type | Key | Best For |
|---|---|---|---|
HS256 | Symmetric (HMAC) | Single shared secret | Simple setups where issuer and verifier are the same service |
RS256 | Asymmetric (RSA) | Private key signs, public key verifies | Distributed systems, SSO — verifiers never need the private key |
ES256 | Asymmetric (ECDSA) | Private key signs, public key verifies | Same as RS256 but with smaller keys and faster operations |
Symmetric (HS256) is simpler to set up: both the issuer and the verifier share the same secret. The downside is that every service that needs to verify tokens must have access to the secret, which widens the attack surface.
Asymmetric (RS256, ES256) is better for distributed architectures. The auth server signs tokens with a private key, and any service can verify them using the corresponding public key — which is safe to distribute. ES256 is the modern choice: equivalent security to RS256 with smaller tokens and faster verification.
Security Best Practices
JWTs are a powerful tool, but they are easy to misuse. Follow these rules:
- Never store secrets in the payload. The payload is encoded, not encrypted. Anyone can decode it. If you need encrypted tokens, use JWE (JSON Web Encryption) instead.
- Use short expiration times. Access tokens should expire in 5–15 minutes. Use refresh tokens for longer sessions. Short-lived tokens limit the damage if one is stolen.
- Always use HTTPS. JWTs sent over HTTP can be intercepted in transit. HTTPS is non-negotiable.
- Validate all claims. Always check
exp,iss, andaud. Do not just verify the signature — an expired or misrouted token should be rejected. - Do not accept
alg: none. Some libraries accept unsigned tokens if the header says"alg": "none". Explicitly reject this algorithm in your verifier configuration. - Use an allowlist for algorithms. Configure your verifier to accept only the specific algorithm(s) you expect (e.g., only RS256). This prevents algorithm confusion attacks.
- Store tokens securely.
HttpOnlycookies are more secure thanlocalStoragebecause they are not accessible via JavaScript (mitigating XSS attacks). If you uselocalStorage, understand the trade-offs.
Common JWT Mistakes
These are patterns that show up in security audits repeatedly:
- Putting the JWT in the URL. Query strings end up in server logs, browser history, and referrer headers. Use the
Authorizationheader or a cookie instead. - Not validating the signature at all. Some developers decode the payload with a Base64 library and skip verification. This means an attacker can forge any token.
- Using a weak secret for HS256. If your shared secret is
"secret"or"password123", it can be brute-forced in seconds. Use a cryptographically random string of at least 256 bits. - Tokens that never expire. If a token has no
expclaim, it is valid forever. If that token leaks, you have no way to revoke it without changing the signing key (which invalidates all tokens). - Storing too much data. JWTs are sent with every HTTP request. A 4KB token adds overhead to every API call. Keep the payload lean.
Token Refresh Flow
Short-lived access tokens expire quickly, so you need a way to get new ones without forcing the user to log in again. This is the refresh token flow:
- The user logs in. The server returns an access token (short-lived, e.g., 15 minutes) and a refresh token (long-lived, e.g., 7 days).
- The client sends the access token with every API request in the
Authorization: Bearerheader. - When the access token expires, the API returns a
401 Unauthorizedresponse. - The client sends the refresh token to a dedicated
/refreshendpoint. - The server validates the refresh token, issues a new access token (and optionally a new refresh token — this is called refresh token rotation), and returns them.
- The client retries the original request with the new access token.
Refresh token rotation is a security best practice: each refresh token can only be used once. If an attacker steals a refresh token and the legitimate user also tries to use it, the server detects the reuse and invalidates the entire session.
// Simplified refresh flow (client-side)
const handleApiRequest = async (url, options) => {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${getAccessToken()}`,
},
});
if (response.status === 401) {
const refreshResponse = await fetch("/api/refresh", {
method: "POST",
body: JSON.stringify({ refresh_token: getRefreshToken() }),
});
if (refreshResponse.ok) {
const { access_token, refresh_token } = await refreshResponse.json();
saveTokens(access_token, refresh_token);
response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${access_token}`,
},
});
} else {
redirectToLogin();
}
}
return response;
};JWTs vs. Sessions: When to Use Which
This is one of the most debated topics in web development. Both approaches have legitimate use cases:
| Aspect | JWT | Server-Side Sessions |
|---|---|---|
| State storage | Client (stateless server) | Server (session store / database) |
| Scalability | Easy — no shared state between servers | Requires shared session store (Redis, DB) |
| Revocation | Hard — requires a blocklist or short expiry | Easy — delete the session from the store |
| Payload size | Larger (carries claims in every request) | Smaller (just a session ID cookie) |
| Cross-domain | Works well (sent as Bearer token) | Requires CORS cookie configuration |
| Microservices | Ideal — each service verifies independently | Each service must query the session store |
| Simplicity | More moving parts (signing, refresh flow) | Simpler to implement correctly |
Use JWTs when you need stateless authentication across distributed services, mobile apps, or third-party API consumers.
Use sessionswhen you need easy revocation (e.g., "log out all devices"), are running a single monolithic app, or want the simplest secure option for a traditional web app.
Try it yourself: Decode and inspect any JWT instantly with our JWT Decoder — no data leaves your browser.