Development

How to Decode and Debug a JWT

A JSON Web Token is just three Base64URL-encoded parts. Once you can read them, most “invalid token” errors explain themselves in seconds. This guide shows how to decode a JWT, check the claims that usually break, and the mistakes that turn JWTs into security holes.

Beginner · 7 min read · Updated October 30, 2026

What a JWT is made of

A JWT looks like xxxxx.yyyyy.zzzzz. The three parts, separated by dots, are:

  1. Header — JSON with the signing algorithm (alg) and token type, for example {"alg":"HS256","typ":"JWT"}.
  2. Payload — JSON with the claims: who the token is about and when it is valid.
  3. Signature — a hash of the first two parts made with a secret (HMAC) or a private key (RSA, ECDSA).

The first two parts are only encoded, not encrypted. Anyone holding the token can read them, which is why you should never put passwords or personal data in a JWT payload.

Decode a token

Paste the token into the JWT Decoder and it shows the header, the payload and every timestamp as a readable date. It runs in your browser, so production tokens are not sent anywhere. To do it by hand, Base64URL-decode each part — replace - with +, _ with /, add padding — or in Node:

const [header, payload] = token.split('.').slice(0, 2)
  .map(part => JSON.parse(Buffer.from(part, 'base64url').toString()));
console.log(header, payload);

The claims that break most often

ClaimMeaningTypical error
expExpiry, Unix seconds“jwt expired” — check the server and client clocks too
nbfNot valid beforeA token used a few seconds early because the issuer’s clock runs ahead
iatIssued atRejected as “issued in the future” after clock drift
audAudience the token is meant forA token for one API sent to another — the verifier must check it
issIssuerDev and production issuers mixed up

Timestamps are in seconds, not milliseconds. If an expiry looks like the year 55,000, someone wrote Date.now() instead of Math.floor(Date.now() / 1000). The Unix Timestamp Converter settles it. Most libraries accept a small clock tolerance (30–60 seconds) to absorb drift between servers.

Why “invalid signature”?

  • Wrong secret or key — the most common cause. Check the environment variable on the server that verifies, not just the one that signs.
  • Wrong algorithm — a token signed with RS256 verified as HS256, or the reverse.
  • The token was edited — changing a single character of the payload breaks the signature, which is the point.
  • Key rotation — the issuer’s keys changed. With JWKS, match the kid in the header to a published key.

To check an HS256 token by hand, compute HMAC-SHA256 over header.payload with the secret in the HMAC Generator and compare it, Base64URL-encoded, with the third part.

Security mistakes to avoid

  • Decoding is not verifying. Never trust claims from jwt.decode(); use your library’s verify function with an explicit list of allowed algorithms.
  • Reject alg: none and pin the algorithm — never let the token choose how it is checked.
  • Keep tokens short-lived (minutes for access tokens) and use refresh tokens for long sessions; a JWT can’t be revoked before it expires without extra server state.
  • Don’t store tokens in localStorage when an XSS bug could read them; an HttpOnly, Secure, SameSite cookie is safer for browser apps.

Need a test token with specific claims? Create one with the JWT Encoder.

Frequently asked questions

Can anyone read my JWT?
Yes. The header and payload are only Base64URL-encoded. The signature stops people from changing them, not from reading them. Use JWE (encrypted tokens) if the contents must be secret.
Is it safe to paste a token into an online decoder?
Only if it runs locally. devpack’s JWT Decoder decodes in your browser and sends nothing; avoid tools that post the token to a server, especially for production tokens.
What does “jwt malformed” mean?
The string isn’t three dot-separated Base64URL parts — often because “Bearer ” was left in front of it, or the token was cut short when copied.

Keep reading