If you've spent any time building web APIs, you've probably pasted a JWT into a decoder at least once — usually right after getting a 401 Unauthorized error that made no sense, or trying to figure out why a token your backend issued isn't being accepted by a downstream service.
JWTs are everywhere. Every major auth system — Auth0, Firebase Auth, Supabase, AWS Cognito, Keycloak, Google OAuth — issues them. REST APIs use them as Bearer tokens. WebSocket connections authenticate with them. And yet the format confuses developers at every experience level, because the token looks like encrypted nonsense until you know the pattern.
This guide covers everything: what the three parts actually contain, what each standard claim means, how the signature algorithms work, how to debug common JWT errors, and how to use the free browser-based JWT decoder, verifier, and builder.
What a JWT Actually Is
JWT stands for JSON Web Token. It's a compact, URL-safe string that encodes a JSON object along with a cryptographic signature. The full spec is RFC 7519, but the practical summary is: a JWT lets a server send structured data to a client (or another server) in a format that can be verified without any database lookup.
The defining characteristic is the three dots. Every JWT has exactly two periods separating three Base64URL-encoded segments:
- Red — Header: algorithm and token type
- Blue — Payload: the actual claims (user ID, roles, timestamps)
- Green — Signature: cryptographic proof the token was not tampered with
The crucial thing to understand: Base64URL encoding is not encryption. Anyone who has a JWT can decode the header and payload without any key. The signature only proves the token was created by someone with the signing key and has not been modified since. It does not hide the contents.
Part 1 — The Header
Decode the first segment and you get a small JSON object. Almost always:
The alg field declares which algorithm was used to sign the token. This is critical for verification — you must use the same algorithm to verify as was used to sign. Some tokens include a kid (Key ID) field — a hint about which specific key from a JWKS endpoint was used, allowing auth servers to rotate keys without breaking existing tokens.
Part 2 — The Payload and Standard Claims
The payload is where the useful data lives. RFC 7519 defines registered claim names — standard fields with well-known meanings:
| Claim | Full Name | Type | What It Means |
|---|---|---|---|
| iss | Issuer | string/URI | The server that issued the token |
| sub | Subject | string | Who the token is about — typically a user ID |
| aud | Audience | string/array | The service(s) this token is intended for |
| exp | Expiration Time | Unix timestamp | Token must be rejected after this time |
| nbf | Not Before | Unix timestamp | Token must be rejected before this time |
| iat | Issued At | Unix timestamp | When the token was created |
| jti | JWT ID | string | Unique ID to prevent replay attacks |
Why Timestamp Claims Cause Most 401 Errors
The exp, iat, and nbf claims are stored as Unix timestamps — seconds since January 1, 1970. A raw value of 1769536000 is meaningless at a glance. The JWT decoder converts these to readable dates and shows a status badge — Expired, Valid, or Not Yet Active.
The most common cause of JWT debugging sessions is a token that was valid when the developer tested it, but by the time a colleague checked the logs, the token had expired. Seeing the actual expiry date instantly resolves this without any mental arithmetic.
Part 3 — The Signature
The signature is computed over the first two parts of the JWT (Base64URL(header) + "." + Base64URL(payload)). Any change to either part — even a single character — produces a completely different signature. This prevents tampering.
What the signature does NOT do: it does not hide the header or payload. Anyone with the token can read the payload. It only proves that whoever produced the signature had the correct key at signing time. This is why you should never store sensitive data — passwords, full PII, private keys — in a JWT payload.
JWT Algorithms — HS256, RS256, ES256 and the Rest
| Algorithm | Family | Key Type | Best For |
|---|---|---|---|
| HS256 / HS384 / HS512 | HMAC | Shared secret | Single-server apps, internal services |
| RS256 / RS384 / RS512 | RSA | Private + Public key pair | Multi-service, OIDC providers |
| ES256 / ES384 / ES512 | ECDSA | EC Private + Public key | Performance-sensitive, smaller signatures |
| PS256 / PS384 / PS512 | RSA-PSS | Private + Public key pair | FIPS/compliance environments |
| none | — | No key | Never use in production |
HS256 vs RS256 — The Key Architectural Choice
With HS256, a single shared secret is used for both signing and verification. Any service that can verify a token can also create one. This is fine for a single backend, but in a microservices architecture it means every service that needs to verify tokens also has the signing key — a significant security surface area if any service is compromised.
With RS256, the auth server signs tokens with a private key. Every other service verifies with the corresponding public key, which can be distributed freely — often published at a JWKS endpoint like https://auth.example.com/.well-known/jwks.json. A compromised downstream service can read tokens but cannot forge them. This is why RS256 is the standard for Auth0, Firebase, Cognito, and all major OIDC providers.
ES256 (ECDSA) is the modern alternative to RS256 — same asymmetric benefits but much smaller signatures (64 bytes vs 256 bytes) and faster computation. If you're building a new system and performance or bandwidth matters, ES256 is worth evaluating.
The alg:none Attack — Unsigned Tokens Are Not Safe
In 2015, researchers discovered that many JWT libraries would accept tokens with alg: "none" and no signature as valid. The attack is straightforward: take any valid token, change the header's alg to "none", modify the payload (grant admin role, change user ID, extend expiry), strip the signature entirely, and re-encode. A vulnerable server accepts the forged token.
The fix is simple but must be explicit: always specify allowed algorithms in your validation code. In Node.js jsonwebtoken: jwt.verify(token, secret, { algorithms: ['HS256'] }). Never rely on a library to make the right default choice — explicitly reject algorithms you don't use.
Using the JWT Tool — Three Tabs Explained
Tab 1 — Decode
Paste any JWT from an Authorization header, browser DevTools, an API response, or a log file. The decoded output appears instantly, showing: the algorithm with a plain-English description, every payload claim labeled for the 25+ known standard and OIDC claims, timestamp claims converted to readable dates with a validity status badge (Expired / Valid / Not Yet Active), and the raw JSON copyable with one click.
Use this tab for: debugging auth errors, verifying what claims an OIDC provider is sending, checking token expiry without manually converting Unix timestamps.
Tab 2 — Verify Signature
Switch to Verify after decoding to cryptographically confirm the signature. For HMAC tokens, paste your shared secret. For RSA or ECDSA tokens, paste the PEM-formatted public key. Verification uses the browser's native Web Crypto API — no data is sent to any server. The result shows either Valid Signature or Invalid Signature clearly.
Use this tab for: confirming a token was signed by the expected key, catching configuration mismatches (wrong secret, wrong key) during development, and validating tokens from third-party OIDC providers.
Tab 3 — Build a JWT
Select an algorithm (HS256, HS384, or HS512), enter your HMAC secret, add claims using preset buttons (sub, iss, aud, email, roles) or custom key-value pairs, set an expiry duration, and click Build Token. The generated JWT appears immediately for copying into Postman, curl, or a test suite.
Use this tab for: generating test tokens without writing sign() code, verifying your validation logic correctly handles specific claim combinations, and creating sample tokens for API documentation or demos.
Debugging JWT Auth Errors — Six Steps
When a 401 Unauthorized hits and you're not sure why, follow this sequence:
- Step 1: Extract the token from the Authorization header (
Bearer <token>) or cookie. - Step 2: Paste into the decoder. Check
exp— is it in the past? Expired token is the most common cause. - Step 3: Check
aud— does the audience match what your server expects? A token issued forapi.example.comwill fail validation onapp.example.com. - Step 4: Check
iss— does the issuer match your auth server's configured issuer? A trailing slash mismatch (https://auth.example.comvshttps://auth.example.com/) will fail strict issuer validation. - Step 5: Check the
algin the header — is it what your server's validation config expects? - Step 6: Use the Verify tab with your secret or public key to confirm the signature is intact and matches the key you expect.
JWT vs Session Cookie
| Factor | JWT | Session Cookie |
|---|---|---|
| State | Stateless — no DB lookup | Stateful — server stores session data |
| Horizontal scaling | Easy — any server can verify | Requires shared session store (Redis) |
| Instant revocation | Hard — needs a token blocklist | Easy — delete from store |
| XSS risk | High if stored in localStorage | Low with HttpOnly cookie |
| Cross-domain use | Easy — Authorization header | Requires CORS + SameSite config |
JWT Security Checklist
- Always specify allowed algorithms. Never let the library decide based on the alg header value — that is the alg:none attack vector.
- Keep access token expiry short. 15–60 minutes is standard. Use refresh tokens for longer sessions.
- Use RS256 or ES256 in multi-service architectures. Distributing your HMAC signing secret to every microservice is a security risk.
- Never store sensitive data in the payload. Passwords, full PII, and secrets are readable by anyone with the token.
- Store in HttpOnly cookies, not localStorage. localStorage is XSS-accessible. HttpOnly cookies are not.
- Validate all relevant claims. Check exp, iss, and aud — not just the signature.
- Use a token blocklist for logout. JWTs cannot be invalidated mid-lifetime without one.
Final Thoughts
JWTs look complicated at first glance — three segments of Base64 text that seem like gibberish. But the structure is simple and consistent: the header tells you the algorithm, the payload carries the claims, and the signature proves nothing was changed. Once that pattern is clear, every JWT you encounter becomes immediately readable.
The nuances — which algorithm to choose for which architecture, what exp and aud and iss mean and why, the alg:none vulnerability, the localStorage vs cookie tradeoff — are what separate developers who work with JWTs confidently from developers who spend hours tracing auth bugs.
The free JWT decoder, verifier, and builder handles the mechanical work: paste a token and see everything decoded with labels and expiry status; verify a signature against your secret or public key; build a test token for Postman without writing a line of code. Everything runs in your browser — no data is sent anywhere. That's the right tool for working with JWTs in 2026.