DEV Community

zhihu wu
zhihu wu

Posted on Originally published at codetoolbox.pro

Your JWT Is Not Encrypted - Here's What's Actually Inside It

A JWT looks like encrypted gibberish: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx.... Here is the uncomfortable part — it is not encrypted. Anyone holding the token can read everything in it. That is by design, and it is also why a surprising number of "why is auth broken?" bugs get solved in about ten seconds once you actually look inside the token.

Three segments, no secrets

A JWT is header.payload.signature, each part Base64URL-encoded, joined by dots. Base64URL is just an encoding (URL-safe alphabet, padding stripped so it can live in a header, cookie, or query string). The first two segments are plain JSON. The third is a signature over the first two — a signature proves nobody tampered with the token, it does not hide anything.

Decode one in Node, no dependencies:

const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc123';
const [, payload] = token.split('.');
const claims = JSON.parse(Buffer.from(payload, 'base64url'));
console.log(claims);
Enter fullscreen mode Exit fullscreen mode

Python:

import base64, json
payload = token.split('.')[1]
claims = json.loads(base64.urlsafe_b64decode(payload + '==='))
Enter fullscreen mode Exit fullscreen mode

What to check when auth fails

Almost every JWT bug is one of these claims, and they are all Unix seconds:

  • exp — expiry. Expired tokens produce a 401 that has nothing to do with your code.
  • nbf — "not before". A token used too early is invalid, which bites when servers' clocks drift.
  • aud / iss — audience and issuer. One service rejecting a token issued for a different audience is extremely common in multi-service setups.

Convert exp and compare against the clock before you start rewriting middleware.

The bug that is not a bug: alg: none

The header carries alg, and the single worst JWT mistake is letting the client tell the server which algorithm to trust. If a server honours "alg": "none", an attacker deletes the signature and forges any payload they want. A subtler variant is algorithm confusion: an RS256 token re-signed as HS256 using the public key as the HMAC secret. Fix both by pinning the accepted algorithms server-side and always verifying with a real library (jose, PyJWT, jsonwebtoken) — never by decoding and trusting.

Don't put anything private in the payload

Roles, internal IDs, emails, feature flags — all readable by whoever holds the token, including anyone who finds it in a log or a URL. Keep the payload minimal; look up the rest server-side.

When I'm debugging one of these, I decode the token first. I've been using CodeToolbox's JWT decoder — it runs entirely in the browser (no token leaves your machine), prints the header and claims, and converts exp/iat/nbf into readable local time so I can see instantly whether the token is expired. It does not verify signatures, which is the honest limitation of any browser-side tool: verification needs the key and belongs on your server.

Token debugging tip of the day: read the claims before you touch the code. Most of the time, the token was simply expired.

Top comments (0)