Guides · Utiltown

JWT: Decoding vs Verifying — What to Trust

Every developer has done it: paste a JWT into a decoder, read the payload, and act on what it says — exp passed, role missing, iss unexpected. The workflow is legitimate and useful, but the boundary between reading a token and trusting one is where real vulnerabilities live. This guide draws that line precisely: what decoding is, what verification adds, which attacks each step stops, and when browser-side inspection is the right tool.

The three parts, and what each can lie about

A JWT is three base64url segments joined by dots: header.payload.signature. The first two are not encrypted — decoding is trivial, which is the point: JWTs are signed, not secret. The third segment is what turns text into a credential:

What decoding gives you — and what it never can

Decoding is base64url decode + JSON parse. That is all. It answers debugging questions instantly:

What decoding can never do: prove the payload came from the issuer. Anyone with a text editor can produce {"alg":"none","role":"admin"} and a matching signature format. If any part of your client treats decoded claims as authorization — hiding admin routes because a decoded role says so, for example — you have built a UI veneer over an open door. The server must verify every request regardless of what the client believes.

What verification adds

Verification is server-side work (or explicit, library-backed validation of ID tokens in OIDC flows). It binds the payload to a key:

  1. Check the signature against the issuer's key — for RS/ES algorithms, the public key from the issuer's JWKS endpoint (matched by kid); for HS256, a shared secret that must never have left the server.
  2. Pin the algorithm. Never trust the header's alg blindly. Configure the expected algorithm (algorithms: ['RS256']) so a crafted alg: none or a key-confusion downgrade is rejected before any crypto runs.
  3. Validate registered claims: exp and nbf against the current time, iss against the expected issuer, aud against your client id.
Threat Stopped by decoding? Stopped by verification?
Forged payload (role=admin) no yes — signature mismatch
alg: none unsigned token no yes — pinned algorithm rejects it
RS256→HS256 downgrade no yes — a public key is not a valid HMAC secret
Expired-token replay no yes — exp check (but see revocation below)
Wrong-audience token reuse no yes — aud/iss pinning
Confidentiality of claims neither — JWTs are signed, not encrypted; use JWE or keep payloads minimal

Debugging with a browser decoder, safely

The everyday loop — 401 from an API, paste the bearer token into a decoder, read the claims — is fine if two hygiene rules hold:

After verification: the revocation gap

Even a perfectly verified token has a known limitation: exp is checked once at verification, not re-checked at every use. The standard mitigations, in order of prevalence:

Practical checklist

When the payload holds timestamps you need to correlate with server logs, the Unix Timestamp Converter turns iat/exp into readable UTC, and the JSON Formatter pretty-prints the decoded payload for a line-by-line review.

Frequently Asked Questions

Does decoding a JWT prove it is valid?

No. Decoding is just base64url plus JSON parsing: anyone can read the header and payload of any token, and anyone can forge a token with any claims inside. Only signature verification — checking the signature against the issuer’s key — proves the payload came from the issuer and was not modified. A decoded JWT is a claim; a verified JWT is evidence.

When is it safe to inspect a JWT in a browser tool?

When you are debugging your own token and understand that nothing you see has been verified. Reading claims (exp, sub, scopes) to diagnose why a request failed is legitimate, everyday work. The risk is behavioral: never let client-side code make authorization decisions from decoded claims — an attacker can edit a payload, re-encode, and claim any role. The Utiltown JWT Decoder runs locally and never sends tokens anywhere, which matters because pasting live tokens into random websites is itself a credential leak.

What does signature verification actually check?

Three things, all required: (1) the signature matches the header and payload bytes under the algorithm, using the key the issuer would have used; (2) the alg in the header is one you expect — blocking the alg:none and RS256→HS256 downgrade attacks; (3) the registered claims hold: exp/nbf in the valid window, iss matches the expected issuer, aud matches your client. Skipping the algorithm pin is how the classic JWT library CVEs happened.

Why is exp not enough even after verification?

exp bounds the token’s lifetime, not its revocation. A verified token stolen one minute after issue stays valid until exp unless the server checks a denylist or you use short-lived access tokens with refresh rotation. Treat exp as an upper bound on exposure, not as a logout mechanism.

Should I store JWTs in localStorage or cookies?

Both have a specific attack to design against. localStorage is readable by any script on the page, so one XSS becomes token theft — mitigate with short expirations, or keep the token in memory only. HttpOnly cookies are unreadable to scripts but need CSRF protection and SameSite configuration. What is never acceptable is pasting production tokens into third-party sites that could log them.