Security Hygiene beginner 8 min

Inspect a JWT From a Failing API Request

Decode a bearer token, read its claims, and check why auth works in one environment but fails in another.

Scenario

A request that works locally returns 401 Unauthorized in staging, and the only difference anyone can name is "the token." A JWT is three Base64URL segments — header, payload, signature — and the payload is readable by anyone who holds the token: no secret needed, decoding is not decryption. That readability is a debugging gift: exp, aud, and sub usually explain a 401 faster than any server log.

Broken input

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3NjcyMjU2MDAsImF1ZCI6ImFwaSJ9.signature

Goal

Use JWT Decoder & Inspector to read the token's claims and check them against what staging expects — while remembering that decoding shows what the token says, and only signature verification (server-side, with the key) proves what it is.

Tool sequence

  1. Open JWT Decoder & Inspector.
  2. Paste the bearer token from the failing request's Authorization header.
  3. Read the header first — alg: HS256, typ: JWT — then the payload claims: sub: "123", aud: "api", exp: 1767225600.
  4. Check the two usual 401 suspects: convert exp with Timestamp Converter (1767225600 = 2026-01-01 00:00 UTC — expired if staging's clock is past it), and compare aud against the audience staging validates (a token minted for local api often fails a staging audience check).
  5. If claims all look right, the failure is signature-side — wrong signing key or environment mismatch — which no client-side decoder can confirm; that check belongs to the server.

Checkpoint output

{
  "header":  { "alg": "HS256", "typ": "JWT" },
  "payload": { "sub": "123", "aud": "api", "exp": 1767225600 }
}

The claims are readable without any key: subject 123, audience api, expiry at 2026-01-01 UTC. Whether staging accepts them is a verification question the decoder deliberately cannot answer.

Common mistakes

  • Assuming decoded claims prove the token is trusted — decoding skips the signature entirely; forged payloads decode just as nicely.
  • Forgetting JWT payloads are Base64URL-encoded, not encrypted — never put secrets in claims.
  • Reading exp as a raw number and skipping the conversion, missing that the token expired an hour ago.
  • Comparing tokens between environments by eyeballing the middle segment instead of decoding both and diffing the claims.

Related tools and lessons