The first thing to understand about a JSON Web Token is that it is not secret. It is signed, not encrypted. Anyone holding one can read everything inside it with no key and no special access — which is exactly why putting anything sensitive in a JWT payload is a mistake people keep making.

You can decode one right now with the JWT Decoder. It runs entirely in your browser, which matters when the token you are debugging is a live session credential.

The three parts

A JWT is three base64url-encoded segments separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkppbSJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header, payload, signature. The first two decode straight to JSON. The third is the cryptographic proof.

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

alg is the signing algorithm. typ is nearly always JWT. You may also see kid, a key identifier telling the verifier which key to use when several are in rotation.

Payload

{
  "sub": "1234567890",
  "name": "Jim Halpert",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

These are claims. Some are registered standard claims, the rest are whatever the application chose to include.

Signature

Computed over the header and payload using a secret (HMAC) or a private key (RSA/ECDSA). It proves the token has not been altered. It does not hide anything — the decoder above needs no key to show you the contents, and neither does anyone else.

The claims worth knowing

exp — expiry, as a Unix timestamp in seconds. Past this, the token should be rejected.

iat — issued at. Useful for spotting tokens with implausibly long lifetimes.

nbf — not before. The token is invalid until this time. Rare, and a source of confusing failures when clocks drift.

sub — subject, normally the user ID.

iss — issuer. Which service minted it.

aud — audience. Which service is supposed to accept it. Worth checking: a token issued for one service being accepted by another is a real vulnerability class.

jti — a unique token ID, used for replay prevention and revocation lists.

Those timestamps are seconds, not milliseconds. If you paste one into a JavaScript Date without multiplying by 1000, you get 1970 and briefly panic. Everyone does this once.

What to actually check

Is anything sensitive in the payload?

The most common real-world problem. Because it decodes with no key, anything in the payload is effectively public to whoever holds the token. Email addresses are borderline; internal user IDs are usually fine; full names, phone numbers, addresses, permission structures and — genuinely seen in the wild — password hashes are not.

Is the lifetime sane?

Subtract iat from exp. An access token good for fifteen minutes is normal. One good for thirty days is a problem, because JWTs are typically stateless: there is no server-side session to invalidate, so a leaked token stays valid until it expires. That is the trade-off statelessness buys you.

Is alg what you expect?

Two classic attacks live here.

alg: none — the specification permits an unsigned token. A verifier that trusts the header's algorithm claim can be handed a token with alg set to none and no signature, and will accept it. Every mature library blocks this now, but hand-rolled verification still gets it wrong.

RS256 downgraded to HS256 — subtler. RS256 verifies with a public key. HS256 verifies with a shared secret. If an attacker changes alg to HS256 and signs the token using the public key as the HMAC secret, a naive verifier that picks its algorithm from the header will validate it — using a key the attacker already has.

The defence for both is the same: the server decides which algorithm is acceptable. It never takes that instruction from the token.

Is the secret weak?

HS256 tokens signed with something like secret, changeme or an application name can be brute-forced offline in seconds. If you are reviewing your own system and the signing secret is short or guessable, treat it as a live finding — anyone with one valid token can forge any token they like, including one that says "role": "admin".

Decoding safely

A JWT you are debugging is often a working credential. Pasting a live session token into a random website means handing that site your session.

Use a decoder that runs locally. The BitCops JWT Decoder does the decoding in your browser and sends nothing anywhere — you can confirm that by opening your network tab and watching it make no requests. That is the property to look for in any token tool, ours included.

Common questions

Is a JWT encrypted?
No. It is signed. The payload is base64url — readable by anyone with the token. Encrypted variants exist (JWE) but are much less common.

Can I decode a JWT without the secret?
Yes. The secret is needed to verify the signature, not to read the contents.

Why does my exp show 1970?
JWT timestamps are in seconds; JavaScript expects milliseconds. Multiply by 1000.

Can a JWT be revoked?
Not natively — that is the cost of statelessness. You need a denylist keyed on jti, or short expiry plus refresh tokens.

Where to go next