JWT is decoded, not verified — and the difference is how authentication breaks
A JWT (JSON Web Token) is three Base64url-encoded JSON objects separated by dots: header.payload.signature. The decoder on this page shows you what is in a token. It does not tell you whether the token is trustworthy. Those are different operations, and confusing them is how authentication systems have been broken in production for the last decade.
Decoding is free; verification requires a key
Anyone can decode any JWT — including this tool, the attacker, and the user’s browser DevTools. Base64url is not encryption. The payload of every JWT you have ever issued is readable to whoever holds the token. Verification, on the other hand, requires the signing key and recomputing the signature over header.payload. Only the server that issued the token (or holds the public key, for asymmetric algorithms) can do that.
Algorithms: HS256, RS256, ES256, and the “none” trap
HS256: HMAC with SHA-256, symmetric (same key signs and verifies). Fast, simple, but the signing key must be held only by the issuer — if a client needs to verify, you need RS256. RS256: RSA, asymmetric. Public key can verify, only private key can sign. ES256: ECDSA with P-256, asymmetric, smaller signatures than RS256. none: the historic disaster — if your library trusts the alg field from the token and the attacker sets it to none, the signature check is skipped. CVE-2015-9235 hit multiple major libraries with this. Lesson: enforce the algorithm on the server side, never let the token decide.
Time claims: exp, nbf, iat
exp (expiration) is a Unix timestamp; tokens after that time must be rejected. nbf (not before) is the earliest valid time. iat (issued at) is informational. Allow a small clock-skew tolerance when validating — the convention is 30 seconds — because client clocks drift. Reject anything older than your refresh-token lifetime regardless of what the token says.
Audience and issuer: aud, iss, sub
iss identifies the issuer; verify it matches the server you trust. aud identifies the intended audience; if you have multiple APIs sharing a signing key, aud prevents a token issued for service A from authenticating to service B (confused-deputy attack). sub identifies the user. Validate all three on every request, not just on first login.
Storage: cookie vs localStorage, and the XSS/CSRF trade-off
Cookies with HttpOnly; Secure; SameSite=Lax are safe from XSS exfiltration but vulnerable to CSRF unless you add anti-CSRF tokens. localStorage is immune to CSRF (the browser does not send it automatically) but readable from any XSS payload on your domain. You cannot have both. Choose based on whether XSS or CSRF is your bigger risk in context — for SPAs with strong CSP and no script injection points, localStorage is defensible; for traditional apps with many forms, cookies + CSRF tokens is the conservative choice.
Why JWTs are bad at revocation
A signed JWT is valid until it expires. Revoking a session before that means either keeping a server-side deny-list (which defeats the “stateless” pitch of JWTs) or accepting that compromised tokens are valid until exp. The practical pattern: short access tokens (5–15 min), longer refresh tokens (days), refresh tokens stored in a database that can be invalidated. The JWT is fast to verify; the refresh is the real session.
Takeaway: The decoder on this page is for debugging — inspecting headers, reading claims, troubleshooting why a request was rejected. It is not, and cannot be, a substitute for server-side verification. To verify a token in real code: (1) hold the signing key server-side, (2) enforce the expected algorithm (reject none and unexpected algorithms), (3) validate exp, nbf, iss, aud, (4) keep a refresh-token deny-list for revocation. Client-side JWT verification is theatre — the client controls the verifier.
Sources: RFC 7519 (JWT) · RFC 7515 (JWS) · OWASP JWT cheatsheet.