A JWT (JSON Web Token) decoder is essential debugging gear for anyone working with modern authentication: OAuth2, OpenID Connect, Auth0, Cognito, Firebase Auth, Keycloak, Supabase, Clerk, custom backends — they all issue JWTs. A JWT looks like an opaque eyJ... string but it's actually three base64url-encoded chunks: a header, a payload (the claims), and a signature. This decoder splits, decodes, and pretty-prints the header and payload entirely in your browser — your token is never sent to a server, never logged, never stored.
Privacy guarantee: production JWTs often contain user IDs, email addresses, role claims, tenant IDs, and other PII. Pasting them into a hosted JWT debugger means sending that data to a third party. This tool decodes everything client-side using atob() and JSON.parse(), so the token stays on your machine.
How JWT Decoding Works
A JWT is structured as header.payload.signature, three base64url-encoded segments joined by dots. The decoder splits on ., base64url-decodes the first two segments, and JSON-parses each. The header typically contains {"alg": "RS256", "typ": "JWT"} (the signing algorithm and type). The payload contains the claims: standard claims like iss (issuer), sub (subject/user ID), aud (audience), exp (expiration), iat (issued at), nbf (not before), plus any custom claims your auth system adds. This tool also flags expired tokens by comparing the exp claim to the current time.
When to Decode a JWT
- Debugging 401 errors — is the token expired? wrong audience? missing a scope?
- Inspecting OAuth flows — what claims is your IdP putting in the access token vs. ID token?
- Building permission systems — verify your roles/scopes claim is making it through.
- Auth migration — comparing JWT structures across providers (Auth0 ↔ Cognito ↔ Firebase).
- Security audits — checking for over-broad claims, long expirations, or sensitive data in the payload.
JWT Decoding in Code
// Browser / Node.js (decode only, no verify)
function decodeJWT(token) {
const [header, payload] = token.split('.').slice(0, 2).map(seg =>
JSON.parse(atob(seg.replace(/-/g, '+').replace(/_/g, '/')))
);
return { header, payload };
}
// For VERIFICATION (production), use jose, jsonwebtoken, or auth0/node-jsonwebtoken
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Decode vs. Verify: A Critical Distinction
This tool decodes JWTs — it splits the token, base64-decodes the header and payload, and shows you the claims. It does NOT verify the signature, because verification requires the issuer's secret (HMAC) or public key (RSA/ECDSA), which you typically don't paste into a debugger. For security-critical use, always verify the signature on the server using a library like jose (Node.js), PyJWT (Python), jwt-go (Go), or your platform's standard JWT library. An unverified JWT can be tampered with — anyone can change the claims and re-encode. Related: decode the base64 parts manually with our Base64 decoder.
Standard JWT Claims You'll See
The payload is just JSON, but a handful of registered claims (RFC 7519) appear in almost every token, and knowing them speeds up debugging. iss (issuer) identifies who minted the token — usually your auth provider's URL. sub (subject) is the user or entity the token is about, typically a stable user ID. aud (audience) names the intended recipient API; if your API rejects a token with a 401 even though it looks valid, a mismatched aud is a common culprit. exp (expiration) and iat (issued-at) are Unix timestamps — this tool flags an expired token automatically by comparing exp to the current time, and you can convert those numbers to human dates with our Unix timestamp converter. nbf (not-before) marks the earliest time a token is valid, and jti (JWT ID) is a unique identifier used to prevent replay. Beyond these, providers add custom claims for roles, scopes, tenant IDs, and email — which is exactly why decoding production tokens in a privacy-respecting, client-side tool matters.
Common JWT Errors
If decoding fails, check that the token has exactly three dot-separated parts (header.payload.signature) — a token with two parts is unsigned (alg: none) and one with extra dots is malformed. “Invalid character” errors during base64 decoding usually mean the segment uses Base64URL (it does) but a copy-paste introduced whitespace or truncation. Finally, a token that decodes fine but is rejected by your backend is almost always either expired (exp in the past), not yet valid (nbf in the future), or signed with the wrong key/algorithm.