You’re debugging an auth issue. The token is right there in the request. Something’s wrong with the claims — you just can’t tell what without seeing inside it. Most engineers open a browser tab and paste it into jwt.io. There’s no need.

A JWT is three base64url-encoded segments. The header and payload are plain JSON — you can read them with a one-liner on any machine. No account, no browser, no sending your token anywhere.

What’s Actually Inside a JWT

Three parts separated by dots: header.payload.signature

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9       ← header
.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyMDAwMDAwMH0  ← payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← signature

The header and payload are base64url-encoded JSON — readable with basic tools. The signature is a cryptographic value; you can’t decode it to get useful content.

Decode It Locally

With jq (cleanest — decodes header and payload in one command):

TOKEN="your.jwt.here"
echo $TOKEN | jq -R 'split(".") | .[0:2] | map(@base64d) | map(fromjson)'

Pure bash (no extra tools required):

echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null

If you get a padding error, add | tr '_-' '/+' before base64 -d — base64url uses different characters than standard base64.

Python (cross-platform fallback):

python3 -c "
import base64, json
token = 'your.jwt.here'
payload = token.split('.')[1]
padding = '=' * (4 - len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload + padding)), indent=2))
"

Example output:

{
  "sub": "user_123",
  "role": "admin",
  "exp": 1720000000,
  "iat": 1719913600
}

Fields worth checking: sub (user ID), exp (expiry as Unix timestamp — run date -d @1720000000 to read it), iat (issued at), aud (audience), iss (issuer). Check the header’s alg field too — it tells you how the signature was created.

What Decoding Does NOT Tell You

This is the part most posts skip.

Decoding a JWT tells you what the token claims. It does not tell you whether those claims can be trusted.

Any JWT can be crafted with any payload. A token claiming "role": "admin" means nothing until the signature is verified — without verification, you have no idea who signed it or whether it’s been tampered with.

One reason this matters: historically, some JWT libraries accepted tokens with "alg": "none" in the header — meaning no signature at all — allowing anyone to forge a token with arbitrary claims. Always check that alg is a real algorithm (RS256, ES256, HS256), never none.

When You Need to Verify (Not Just Decode)

Decoding is for inspection. If you need to confirm a token is authentic and untampered, use a proper verification step:

# jwt-cli: brew install jwt-cli  or  cargo install jwt-cli
jwt decode --secret "your-signing-secret" $TOKEN

Or reach for your language’s JWT library — they handle signature verification correctly where raw base64 decoding cannot.


Next time you hit a JWT auth issue, reach for the one-liner before opening a browser tab. If the payload looks right and auth is still failing, the problem isn’t the claims — it’s the signature verification step.