Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 4 min read

Stop pasting production JWTs into random websites

A JSON Web Token looks opaque, so it is easy to treat it like a secret blob and paste it into the first "JWT debugger" Google shows you. That is a bad habit. A JWT is three base64url segments joined by dots. The first tw

A JSON Web Token looks opaque, so it is easy to treat it like a secret blob and paste it into the first "JWT debugger" Google shows you. That is a bad habit. A JWT is three base64url segments joined by dots. The first two are readable with no key at all. The third is a signature you cannot check without the signing secret or public key. Below is how to decode a token on your own machine, what decoding does and does not prove, and why a hosted debugger is a live credential risk.

I checked these steps against RFC 7519 and RFC 7515, and I ran every command on a disposable HS256 test token I signed locally with a throwaway secret. Nothing below is a production token.

What the three parts are

A compact JWT looks like header.payload.signature.

  • Header: JSON, usually alg (the signing algorithm) and typ (JWT).
  • Payload: JSON claims. Common ones are sub, iss, aud, iat, exp, nbf.
  • Signature: bytes that bind header and payload under a key. You cannot invent a valid one without that key.

Header and payload are base64url-encoded, not encrypted. Anyone who holds the string can read every claim. RFC 7519 says this plainly: the payload is a JSON object encoded for transport. If you put a password or a government ID number in a claim, anyone with the token can read it.

Decode on the command line (no website)

Split on the dots, then decode part 1 (header) and part 2 (payload). Leave part 3 alone unless you are verifying.

Python that handles base64url and missing padding. Put the token in TOKEN, then run:

python3 - <<'PY'
import base64, json, os
token = os.environ["TOKEN"].strip()
h, p, *rest = token.split(".")
def dec(seg):
    pad = "=" * ((4 - len(seg) % 4) % 4)
    return json.loads(base64.urlsafe_b64decode(seg + pad))
print("header:", json.dumps(dec(h), indent=2))
print("payload:", json.dumps(dec(p), indent=2))
print("signature segments:", len(rest))
PY

Export the token first: export TOKEN='header.payload.signature'.

On my test token the header printed alg: HS256 and typ: JWT. The payload printed sub, role, scope, iat and exp exactly as I had signed them. The script never touched a network socket.

Bash with cut and base64

This also works, but you must convert base64url to ordinary base64 first:

TOKEN='paste-your-token-here'
PART=$(printf '%s' "$TOKEN" | cut -d. -f2)
STD=$(printf '%s' "$PART" | tr -- '-_' '+/')
PAD=$(( (4 - ${#STD} % 4) % 4 ))
(printf '%s' "$STD"; printf '=%.0s' $(seq 1 $PAD)) | base64 -d; echo

Use -f1 instead of -f2 for the header.

The gotcha: plain base64 -d often fails

Base64url (RFC 4648 ยง5) uses - and _ instead of + and /, and it drops = padding. Many JWT payload segments contain at least one underscore or hyphen. On those tokens, feeding the raw segment to base64 -d returns invalid input and a partial or empty decode.

I confirmed this with a test payload that encoded with an underscore in the middle: converted with tr -- '-_' '+/' plus padding, base64 -d printed the full JSON; without the conversion it exited non-zero and stopped mid-string. Tokens whose segments happen to use only the shared alphabet can decode by luck. Do not rely on luck. Use urlsafe_b64decode or the tr step above.

Decoding never verifies the signature

Reading the claims tells you what the token says. It does not tell you the token is authentic.

To verify HS256 you need the shared secret. To verify RS256 or ES256 you need the issuer's public key (often from a JWKS URL your server already trusts). Verification belongs on your server, with the key material you control. A browser page that only splits and base64-decodes is doing the read step, not the trust step.

If a tool claims it "validated" a token and you never gave it a key, it lied, or it only checked that the JSON parsed and that exp is in the future. Expiry is a claim inside the payload. An attacker who can mint tokens with your secret can set any expiry they want.

Why pasting into a random website is the wrong default

A bearer JWT is a credential. Whoever holds it can often call your API as that user until it expires or is revoked. Pasting it into a hosted debugger means that site's operators (or anyone who compromised that site) get a replayable token. You cannot unsend it.

Use a local decode for inspection:

  1. Prefer the Python or bash steps above on a machine you control.
  2. If you use a browser page, prefer one that states decoding runs in the tab with no upload, then confirm in DevTools Network that no request carries the token while you paste and decode.
  3. Never paste a live production access token into any site you do not run. Mint a short-lived test token, or redact claims and use a throwaway signature, when you only need to check shape.

I built ToolOcean. Its JWT decoder page says decoding runs in the browser, that the token is not sent to a server, and that decoding only reads the base64url header and payload without verifying the signature. Those three claims match what the page's FAQ states. The tool is at toolocean.co/jwt-decoder. The command-line steps above work whether you open that page or not.

Drafted with AI help, tested and checked by me.

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.