JWT Security: How to Decode Without Leaking Tokens
Practical patterns for inspecting JWTs in development — what's safe, what isn't, and how to avoid the most common mistakes.
JSON Web Tokens are everywhere. They power OAuth flows, authenticate API requests, and carry user identity between microservices. They're also one of the most common sources of security mistakes — not because the format is broken, but because developers often don't understand what a JWT actually guarantees.
What a JWT is (and what it isn't)
A JWT is three base64url-encoded segments joined by dots: header.payload.signature. The header describes the algorithm used to sign the token. The payload contains claims — typically things like sub (subject), exp (expiry), iat (issued-at), nbf (not-before), and arbitrary custom fields. The signature is computed over the header and payload using a secret (HMAC) or private key (RSA, ECDSA).
The critical thing to understand: a JWT is signed, not encrypted. Anyone who gets the token can read every byte of the payload. The signature only guarantees that the token wasn't tampered with — not that it's confidential.
The decode-vs-verify distinction
When you "decode" a JWT, you're just base64-decoding the payload. No cryptographic verification happens. This is fine for:
- Reading claims during debugging
- Inspecting an expired token to see what was in it
- Understanding the structure of a token you're about to test
It is not fine for:
- Authorizing a user
- Trusting any claim in the token
- Skipping server-side validation
For any production authorization decision, you must verify the signature against a trusted public key or shared secret. Never trust a decoded token.
Common mistakes
Mistake 1: Logging JWTs. Your error tracking system probably captures request headers. If JWTs are in the Authorization header, they end up in Sentry, Datadog, or wherever. Use middleware to scrub Authorization headers before logging.
Mistake 2: Storing JWTs in localStorage. Any JavaScript on your page can read localStorage. If a third-party script (analytics, chat widget, ad) gets compromised, it can exfiltrate tokens. Use httpOnly cookies instead.
Mistake 3: Trusting alg: none. Some old JWT libraries allowed the none algorithm, which skips signature verification entirely. Modern libraries reject this by default, but if you're working with old code, double-check your verification configuration explicitly whitelists algorithms.
Mistake 4: Long-lived tokens without rotation. A JWT is valid until its exp. If it leaks, the attacker has access until it expires. Keep exp short (15-60 minutes) and use refresh tokens for longer sessions.
Safe debugging workflow
When you need to inspect a JWT during development, here's the safest workflow:
- Use a local decoder (like TextOpal's JWT Decoder) that runs in your browser. No token is sent to any server.
- Copy only the parts you need (e.g., the
subclaim for correlation) into tickets or chat messages. Never paste the full token. - If you're sharing a token for debugging, use a test token from your dev environment — never a production one.
- After debugging, rotate any tokens that might have been exposed.
Reading the expiry
The exp claim is a Unix timestamp (seconds since epoch). The iat claim tells you when the token was issued. The nbf claim, if present, indicates when the token becomes valid — useful for tokens issued slightly before they should be usable.
A common debugging task: "Is this token still valid?" Check that:
expis in the futureiatis in the past- If
nbfexists, it's in the past
TextOpal's JWT Decoder shows this in a single glance — green for valid, red for expired, yellow for not-yet-valid. The relative time ("expires in 2h 15m") makes it easy to spot tokens that are about to expire.
What's actually safe to put in a JWT
The general rule: don't put anything in a JWT that you wouldn't be comfortable printing on a poster. The token is signed, not encrypted — anyone with the token can read it.
Good claims: sub (user ID), iss (issuer), aud (audience), exp (expiry), scope (permission scopes as strings).
Bad claims: passwords (even hashed), API keys, PII like email or phone, internal database query strings.
If you need to attach private data to a session, use server-side sessions keyed by an opaque session ID — the JWT just carries that ID.
The bottom line
JWTs are a useful primitive, but they're not magic. Treat them as signed, public-readable envelopes — never as encrypted containers. Decode freely for debugging, verify rigorously for authorization, and rotate aggressively when in doubt.