JWT Deep-Dive:
Encode, Decode, Inspect & Security
JWTs power authentication across the modern web. This guide covers their structure, how to create and inspect them, common debugging scenarios, and the security pitfalls that separate a safe implementation from a vulnerable one.
JWTs Are Everywhere — Understanding Them Is Not Optional
JSON Web Tokens power authentication across the modern web. Every time you log into a single-page app, call a mobile API, or authenticate with a third-party service, there's a good chance a JWT is involved. They're compact, self-contained, and stateless — a signed JSON object that proves who you are without the server needing a session store.
But JWTs are also a footgun when used incorrectly. Tokens that never expire. Secrets checked into git. Sensitive data in the payload. "None" algorithm attacks. This guide covers the full JWT lifecycle — how they're structured, how to create and inspect them, and the security practices that separate a secure implementation from a vulnerable one.
JWT Structure: Three Parts, One Token
The Anatomy of a JWT
A JWT looks like three Base64URL-encoded strings separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each part has a distinct role:
- Header (part 1, in red) — JSON object describing the token type and signing algorithm. Typically
{"alg":"HS256","typ":"JWT"}. The alg field tells the verifier how to check the signature. - Payload (part 2, in purple) — JSON object containing the claims. This is the actual data the token carries: user ID, expiration time, issuer, permissions. Anyone who has the token can decode and read the payload — it's only Base64URL-encoded, not encrypted.
- Signature (part 3, in blue) — Cryptographic signature over the first two parts. Created by hashing
header.payloadwith a secret key (for HMAC) or private key (for RSA/ECDSA). This is what prevents tampering.
The JWT Inspector decodes all three parts, analyzes each claim, checks expiration status with a visual indicator, and shows you the token's lifetime on a progress bar. It's the fastest way to understand an unfamiliar JWT.
Standard Claims You Should Know
JWT claims are divided into three categories: registered (standardized by the spec), public (defined by your application but registered to avoid collisions), and private (custom, unregistered). The registered claims are the ones you'll see in every well-formed JWT:
| Claim | Full Name | Meaning |
|---|---|---|
iss | Issuer | Who created the token — usually your auth server's URL |
sub | Subject | Who the token is about — typically the user ID |
aud | Audience | Who the token is intended for — the service that should accept it |
exp | Expiration | Unix timestamp after which the token is invalid |
nbf | Not Before | Unix timestamp before which the token is invalid |
iat | Issued At | When the token was created |
jti | JWT ID | Unique identifier — useful for preventing replay attacks |
Always set exp. A token without an expiration is valid forever. If someone steals it, they have permanent access. Most production systems use short-lived access tokens (5-15 minutes) combined with longer-lived refresh tokens that can be revoked server-side.
Creating JWTs for Testing and Development
Why You'd Generate a JWT Manually
In production, your authentication server generates JWTs after verifying user credentials. But during development and testing, you often need a valid JWT before the auth server exists: testing an API endpoint that requires authentication, debugging middleware that parses JWTs, simulating an expired token to test error handling, or verifying that your frontend correctly handles different JWT states.
Here's what the creation process looks like (simplified):
// 1. Define header and payload
const header = { alg: 'HS256', typ: 'JWT' };
const payload = {
sub: 'user-123',
name: 'Alice',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};
// 2. Base64URL-encode both parts
const b64 = (obj) => btoa(JSON.stringify(obj))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
// 3. Sign with HMAC-SHA256
const input = b64(header) + '.' + b64(payload);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(input));
// Convert signature to Base64URL...
// 4. Concatenate
const jwt = input + '.' + b64Signature;
The JWT Encoder does all of this in your browser using the Web Crypto API. Your secret key never leaves your computer. It supports HS256, HS384, and HS512 — the three HMAC-based algorithms. For testing, set a realistic expiration time and specific claims that match what your API expects.
Inspecting and Debugging JWTs
Beyond Basic Decoding
Any Base64 decoder can show you the JWT header and payload. But understanding a token requires more than raw JSON. The JWT Inspector adds analysis layers that save debugging time:
- Visual expiration check — Green for valid, yellow for expiring within an hour, red for expired. No mental math converting Unix timestamps.
- Human-readable timestamps — iat, exp, and nbf displayed as local time with "issued 5m ago" style labels.
- Token lifetime bar — Shows how much of the token's valid window has elapsed. If it's 90% consumed, you know a refresh is imminent.
- Claim-by-claim breakdown — Each claim listed with its type, value, and description. Standard claims get human-readable explanations; custom claims are clearly marked.
Common Debugging Scenarios
- "401 Unauthorized" with a seemingly valid token
- Check the
expclaim first. Expired tokens are the #1 cause. Then checkaud— your token might be for a different service. Finally, verify the server accepts your algorithm (some servers reject HS256 and require RS256). - "Invalid signature" errors
- This means the token has been modified after signing, or the server is using a different secret. If you're generating test tokens, make sure the secret in the JWT Encoder matches what your server expects.
- Missing claims
- Some frameworks require specific claims. Spring Security often expects
scopeorauthorities. Auth0 includesazpandgty. Check your middleware documentation for required claims.
JWT Security: What You Must Get Right
The "None" Algorithm Attack
The JWT spec allows "alg":"none" — an unsigned token. In 2015-2016, researchers found that many JWT libraries would accept tokens with "alg":"none" and skip signature verification entirely. An attacker could modify the payload (change "admin":false to "admin":true), set the algorithm to "none", remove the signature, and the server would accept it.
Modern libraries have fixed this, but only if you configure them correctly. Always whitelist allowed algorithms. Never let the token's header dictate the algorithm without verifying it's in your allowlist.
Where to Store JWTs
Storage is a contentious topic. Here's the pragmatic summary:
- HttpOnly cookies — Most secure option for web apps. JavaScript can't read them, so XSS can't steal them. Set
SameSite=StrictorLaxandSecurein production. The downside: you need CSRF protection since cookies are sent automatically. - localStorage / sessionStorage — Convenient but vulnerable to XSS. If your site has any XSS vulnerability, an attacker can read the token and impersonate the user. Many SPAs use this approach despite the risk because it's simpler than managing CSRF tokens.
- In-memory (JavaScript variable) — Most secure against XSS, but the token is lost on page refresh. Works well for SPAs that use refresh tokens stored in HttpOnly cookies.
Never Put Secrets in the Payload
The JWT payload is Base64URL-encoded — not encrypted. Anyone with the token can decode and read it. Never include passwords, credit card numbers, API keys, or personally identifiable information in JWT claims. The payload is public to anyone who intercepts the token. If you need to transmit sensitive data, encrypt it separately or use a JWE (JSON Web Encryption) — though JWE adoption is far lower than JWS (signed JWTs).
The JWT Toolchain
Three tools cover the complete JWT workflow: the JWT Decoder for quick header and payload extraction, the JWT Encoder for generating test tokens with any claims and secret, and the JWT Inspector for deep analysis with expiration checking and claim breakdowns. All three run locally in your browser — your tokens and secrets never leave your machine. Together they give you everything you need to build, debug, and understand JWT-based authentication.