JWT Decoder: Read the Header, Payload, and Expiry Without Guessing What Went Wrong

Paste a JWT into jwt.io and you get the split-out header and payload, which is useful, but it stops there. It won’t tell you if the token has actually expired relative to right now, what half the claim abbreviations mean, or why decoding a JWT is nothing like verifying one. That last point trips up a surprising number of people building their first auth system: reading a JWT and trusting a JWT are two completely different things.

What a JWT Actually Is

Three base64url-encoded chunks joined by periods: header.payload.signature. The header names the signing algorithm. The payload holds the claims, meaning whatever data the issuer decided to put in there. The signature is a cryptographic proof, generated using a secret or private key, that the header and payload haven’t been tampered with since they were signed.

The part that catches people off guard: the header and payload are just base64, not encryption. Anyone holding a JWT can decode and read every field in it without knowing any secret at all. That’s by design. A JWT is meant to be readable and verifiable, not private. If you’re storing anything sensitive in a payload, you’ve already made a mistake, and it’s one this tool will help you catch.

Decode a JWT

Header


Payload


Signature (not verified here)

Claims explained

Reading the Claims

A handful of claim names are part of the JWT standard and show up constantly. Everything else in a payload is just whatever the issuer chose to add.

ClaimNameWhat it holds
issIssuerWho created and signed the token, usually a service name or URL.
subSubjectWho the token is about, most commonly a user ID.
audAudienceWho the token is intended for. A resource server should reject a token whose audience doesn’t match itself.
expExpirationUnix timestamp after which the token should be rejected outright.
nbfNot beforeUnix timestamp before which the token isn’t valid yet. Rare in practice.
iatIssued atUnix timestamp of when the token was created.
jtiJWT IDA unique identifier for this specific token, often used to support revocation lists.

Everything else you’ll see, things like role, email, or permissions, is a custom claim. There’s nothing wrong with custom claims. Just remember that anyone can read them, so they should describe the user, not secrets belonging to your system.

Decoding vs. Verifying

This is the distinction that actually matters for security, and it’s the one most quick explanations skip past. Decoding means reading what’s in the token. Verifying means cryptographically confirming the token was actually issued by someone holding the correct secret or private key, and that nothing in it has been altered since.

A tool like this one can decode any JWT you paste in, valid or forged, expired or not, because decoding doesn’t require the secret at all. That’s exactly why decoding a token client-side and trusting its contents is a mistake. Verification has to happen server-side, where the secret or public key actually lives. If your backend accepts whatever claims are sitting in an unverified token, someone can hand-craft a payload claiming to be an admin and your server has no way to tell the difference.

Never put secrets in a JWT payload

Passwords, API keys, internal database IDs meant to stay internal, anything you wouldn’t post publicly: none of it belongs in a JWT payload, because base64 isn’t encryption and anyone can decode it. If you need to pass sensitive data between services, keep it server-side and reference it by an opaque ID instead.

Common JWT Mistakes

Storing a long-lived JWT in localStorage is one of the more common ones. It’s convenient, but it’s readable by any JavaScript running on the page, which makes it a direct target for XSS. An httpOnly cookie, which JavaScript can’t read at all, is the safer default for anything that needs to persist.

Skipping expiration checks server-side is another. Some frameworks verify the signature automatically but leave expiry checking as a separate step, and it’s easy to assume signature verification alone covers everything.

Setting alg: none or accepting it from client input is a classic one too. If your server trusts whatever algorithm the incoming token claims to use, an attacker can submit a token with alg: none and no signature at all, and some poorly configured libraries will accept it. The algorithm should be pinned server-side, never read from the token itself.

And using an overly long expiration “to avoid annoying users with re-logins” trades a UX convenience for a much bigger attack window if a token ever leaks. Short-lived access tokens paired with a separate refresh token flow handle this properly.

FAQ

Is a JWT encrypted?

No. Standard JWTs (JWS) are signed, not encrypted. Anyone can decode the header and payload. If you need the contents to actually be unreadable, you’d need JWE, a different and less common JWT variant.

Can I tell if a JWT is valid just by decoding it?

You can tell if it’s well-formed and check whether exp has passed. You cannot confirm it was legitimately issued or hasn’t been tampered with, since that requires verifying the signature with the correct secret or key, which only the issuing server can do.

What happens if exp is missing from the payload?

The token effectively never expires on its own. Whether that’s actually enforced depends entirely on how the receiving server is configured, but a missing exp is generally a red flag worth investigating.

Why does my JWT fail to decode with an error about invalid characters?

Usually one of two things: the token got truncated somewhere (copy-paste, a header size limit, or logging that cut it off), or what you pasted isn’t actually a JWT, like an opaque session token that happens to also be a long random string.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top