JWT Decoding & Troubleshooting
Why Decode JWTs?
JWT (JSON Web Token) is the standard for stateless auth. But when something breaks — 401 errors, expired tokens, wrong claims, CORS issues — you need to see what's inside. Without a decoder, you're guessing.
DevStackIO's JWT Decoder lets you paste any token and instantly see:
- Header — Algorithm, token type, key ID
- Payload — Claims (sub, exp, iat, roles, permissions, custom)
- Signature — Verification status (if public key provided)
All client-side. Your tokens never leave your browser.
JWT Anatomy Refresher
A JWT is three Base64URL-encoded parts joined by dots:
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └─ Signature
│ └─────── Payload (claims)
└───────────── Header
Header (Example)
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-01-01"
}
| Field | Purpose |
|---|---|
alg |
Signing algorithm (HS256, RS256, ES256, EdDSA) |
typ |
Always "JWT" |
kid |
Key ID for key rotation |
Payload / Claims (Example)
{
"sub": "user_12345",
"name": "Alice",
"role": "admin",
"permissions": ["read", "write", "delete"],
"iat": 1704067200,
"exp": 1704153600,
"nbf": 1704067200,
"iss": "https://auth.example.com",
"aud": "api.example.com",
"jti": "abc123def456"
}
| Claim | Name | Required? | Description |
|---|---|---|---|
iss |
Issuer | No | Who created the token |
sub |
Subject | No | User/entity ID (unique) |
aud |
Audience | No | Who the token is for |
exp |
Expiration | Yes (best practice) | Unix timestamp — must be future |
nbf |
Not Before | No | Token not valid before this time |
iat |
Issued At | No | Unix timestamp — when created |
jti |
JWT ID | No | Unique token ID (prevent replay) |
* |
Custom | No | Your app's data (roles, perms, etc.) |
Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Or RSA/ECDSA/EdDSA for asymmetric. Never trust a token without verifying the signature.
Step-by-Step: Decode & Debug a JWT
1. Get the Token
Sources:
- Browser DevTools → Application → Local Storage / Cookies →
access_token - Network tab → Authorization header:
Bearer eyJhbGc... - React/Vue DevTools → State → auth token
- Mobile → Proxy (Charles/Proxyman) → Inspect requests
- Logs → Search for
BeareroreyJ
2. Open the Decoder
tools.devstackio.com/tools/jwt-decoder
3. Paste the Token
- Auto-detects JWT format (3 parts, Base64URL)
- Shows raw parts and decoded JSON side by side
- Highlights syntax errors in red
4. Analyze the Output
Header Tab
- ✅
algmatches expectation (RS256 ≠ HS256) - ✅
kidexists if using key rotation - ⚠️
alg: "none"— Critical vulnerability (reject immediately)
Payload Tab
| Check | What to Look For |
|---|---|
| exp | exp > now? (Green = valid, Red = expired) |
| nbf | nbf < now? (Token not yet valid) |
| iat | Reasonable? (Not years in past/future) |
| iss | Matches your auth server? |
| aud | Includes your API/client ID? |
| sub | Correct user ID? |
| Custom claims | Roles, permissions, tenant, scope present? |
Signature Tab
- No key provided → "Signature verification skipped"
- HS256 → Paste secret → ✅ Valid / ❌ Invalid
- RS256/ES256 → Paste public key (PEM) or JWKS URL → Auto-fetch + verify
- JWKS → Enter
https://auth.example.com/.well-known/jwks.json→ Auto-rotate keys
5. Debug Common Issues
| Symptom | Likely Cause | Decoder Reveals |
|---|---|---|
401 Unauthorized |
Token expired | exp < now |
403 Forbidden |
Missing scope/role | permissions / roles array |
Invalid token |
Wrong algorithm | alg mismatch (HS256 vs RS256) |
Key not found |
kid rotation |
kid in header ≠ JWKS keys |
Audience invalid |
Wrong aud claim |
aud ≠ your API identifier |
Issuer invalid |
Wrong auth server | iss ≠ expected |
Token not yet valid |
Clock skew | nbf > now / iat > now |
Algorithm Deep Dive
| Algorithm | Type | Key | Use Case | Security Notes |
|---|---|---|---|---|
| HS256 | HMAC | Shared secret | Simple apps, single service | Secret must be 256+ bits. Rotate periodically. |
| RS256 | RSA | Public/Private | Standard for auth servers | Public key verifiable by anyone. Key rotation via JWKS. |
| ES256 | ECDSA | Public/Private | Mobile, IoT (smaller keys) | P-256 curve. Faster verify than RSA. |
| EdDSA | Ed25519 | Public/Private | Modern, fast, small | Recommended for new systems. No RNG needed. |
⚠️ Algorithm Confusion Attack
If your server accepts both HS256 and RS256, attacker can:
- Get public key (from JWKS)
- Sign token with HS256 using public key as secret
- Server verifies with HS256 (public key) → Accepts forged token
Fix: Explicitly configure expected algorithm(s). Never auto-detect.
Verifying Signatures in the Decoder
HS256 (Symmetric)
- Select "HS256" in algorithm dropdown
- Paste secret (base64 or raw string)
- Click "Verify" → ✅ Valid / ❌ Invalid
RS256/ES256 (Asymmetric) — Public Key
- Select algorithm
- Paste PEM public key:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
- Or paste JWK:
{"kty":"RSA","e":"AQAB","n":"...","kid":"2024-01"}
- Click "Verify"
JWKS (Key Rotation) — Best for Production
- Enter JWKS URL:
https://auth.example.com/.well-known/jwks.json - Decoder fetches keys, matches
kidfrom header - Verifies with correct key automatically
- Caches for 5 min (configurable)
Time & Clock Skew Handling
JWT times are Unix timestamps (seconds since 1970-01-01 UTC).
The Decoder Shows
- Human readable — "Jan 1, 2024 12:00:00 UTC"
- Relative — "Expired 2 hours ago" / "Expires in 15 minutes"
- Raw —
1704153600
Clock Skew Best Practice
Servers should accept tokens within a leeway window (typically 30-60 seconds):
valid if: (now - leeway) < exp AND (now + leeway) > nbf
Decoder shows "Valid with 30s leeway" if applicable.
Common Time Bugs
| Bug | Symptom | Fix |
|---|---|---|
| Server clock behind | Valid tokens rejected | NTP sync |
| Client clock ahead | Token seems expired early | NTP sync |
exp in milliseconds |
Token expires in year 50000 | Divide by 1000 |
Missing exp |
Token never expires | Always set exp |
Token Debugging Checklist
When a JWT fails, verify in order:
- Format — 3 parts, valid Base64URL, no extra whitespace
- Header —
algexpected,kidpresent if JWKS - Payload —
expfuture,nbfpast,iss/audmatch - Signature — Verifies with correct key/algorithm
- Claims — Required custom claims present (role, tenant, scope)
- Revocation — Check
jtiagainst blocklist (if implemented) - Scope — Token has required permissions for endpoint
Security: What NOT to Put in JWTs
| ❌ Never Put in JWT | Why |
|---|---|
| Passwords / secrets | Anyone with token sees them |
| PII (SSN, DOB, address) | Token may be logged |
| Credit card data | PCI violation |
| Full user objects | Token size, exposure |
| Database IDs (predictable) | Enumeration attacks |
| Internal service URLs | Info disclosure |
| ✅ Safe in JWT | Notes |
|---|---|
User ID (sub) |
Opaque, non-guessable (UUID) |
| Roles / permissions | Minimal set |
| Tenant / org ID | For multi-tenancy |
Session ID (jti) |
For revocation |
Expiration (exp) |
Always |
Related Guides
- JWT Structure — Complete JWT reference
- JWT Security Best Practices — Security deep dive
- HMAC Authentication — API signing
Tools
- JWT Decoder — Paste, inspect, verify. Zero upload, total privacy
- JWT Generator — Create tokens for testing
- Base64 Decoder — Raw part decoding
- JSON Formatter — Pretty-print payload
- Timestamp Converter —
exp/iatto human time
FAQ
Can I decode without verifying? Yes — decoder shows header/payload without key. But never trust unverified tokens in production.
Why does my token have 4 parts? It's likely a nested JWT (JWT inside JWT) or JWE (encrypted). Decoder handles both.
What's x5c in header?
X.509 certificate chain. Used in mTLS / mutual TLS. Decoder shows cert details.
Can I edit and re-encode? Use JWT Generator to create new tokens. Don't manually edit Base64.