JWT anatomy: decode vs. verify

The three segments of a JSON Web Token, what decoding tells you and what it does not, and the claims that matter when debugging auth.

Three segments, two dots

A JWT is header.payload.signature — three Base64URL-encoded segments joined by dots. The header declares the signing algorithm (alg) and token type. The payload carries the claims: the actual data. The signature is a cryptographic proof, computed over the first two segments, that the token was issued by someone holding the key and has not been altered.

All three segments are only encoded, not encrypted. Anyone who sees the token can read the payload — which is why you never put passwords, secrets, or personal data in JWT claims.

Decoding is not verifying

Decoding a JWT — what a debugger does — just Base64-decodes the segments and pretty-prints the JSON. It answers 'what does this token claim?' It does not answer 'is this token genuine?' Verification requires the signing key (or the public key, for asymmetric algorithms like RS256/EdDSA) and recomputing the signature.

This distinction is the most important security fact about JWTs. Client-side decoding is safe and useful for reading expiry or roles; trusting decoded claims server-side without verifying the signature is the bug class behind countless auth bypasses.

The claims you check when debugging

  • iss — issuer: who minted the token. Wrong issuer means you are looking at a token from a different environment.
  • sub — subject: the user or principal the token represents.
  • aud — audience: which service the token is meant for. An aud mismatch is a classic 401 cause.
  • exp — expiration, as a Unix timestamp in seconds. Convert it and compare with your clock.
  • iat / nbf — issued-at and not-before. A token from the future (clock skew) fails validation just like an expired one.
  • scope / roles — authorization detail, entirely application-defined.

Common failure modes

Expired tokens (exp in the past) are the everyday case — check exp against the actual current time, not your assumption of it. Clock skew between issuer and validator breaks nbf and iat checks; most libraries allow a small leeway for this. Algorithm confusion — where a server accepts alg=none or trusts the header's algorithm choice instead of enforcing one — is a real attack, not just a bug: modern libraries forbid it, but custom verification code sometimes does not.

Finally: a JWT is valid until it expires. There is no built-in revocation. Short exp times plus refresh tokens are the standard mitigation; a denylist is the escape hatch.

References

Related tools