Developer

How to Decode a JWT Token (and What's Actually Inside It)

A JWT is three Base64url segments anyone can read. Learn how to decode a JWT, what each standard claim means, and why decoding is not the same as verifying it.

Try it now: JWT Decoder Decode & inspect JWT tokens — free, no signup, runs in your browser.

Your API keeps returning 401 Unauthorized. The token looks fine — it’s there in the request header, it’s long, it starts with eyJ. So you paste it somewhere to look inside, and the answer turns out to be embarrassingly simple: it expired eleven minutes ago.

That is the everyday use of a JWT decoder. But the moment you can read what is inside a token, a much more important question follows: if you can read it, who else can? The answer changes how you should design with JWTs.

The three-part structure

A JSON Web Token is one long string split by two dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSIsImlhdCI6MTc4NDcxNjgwMCwiZXhwIjoxNzg0NzIwNDAwfQ.AJHfPGxth1KcO0ydk3haiLR6QeecbsCGPzZmw8WbKIM

Three segments, each with a job:

  1. Header — metadata about the token itself. Which signing algorithm was used, and what type of token this is.
  2. Payload — the actual data, called claims. Who the user is, when the token expires, what they are allowed to do.
  3. Signature — a cryptographic value proving that the first two parts have not been altered.

Header and payload are just JSON objects. Split on the dots, Base64url-decode the first two segments, and you get readable text:

{ "alg": "HS256", "typ": "JWT" }
{ "sub": "1234", "name": "Ada", "iat": 1784716800, "exp": 1784720400 }

That is the whole “decoding” process. No key, no secret, no server call.

Why it’s Base64url, not Base64

Tokens travel in URLs, headers, and cookies. Standard Base64 uses + and /, and both are hostile in a URL — / reads as a path separator and + is often interpreted as a space in query strings.

Base64url swaps + for - and / for _, and drops the trailing = padding, so a segment can be pasted into a URL without escaping anything.

This also explains a common frustration: if a JWT segment fails or returns garbage in a plain Base64 decoder, the alphabet mismatch is usually why — a tool that understands the variant, like our Base64 Encoder / Decoder, handles it. It is the same encoding that makes almost every JWT start with eyJ, which is simply what {" looks like in Base64.

The standard claims

The payload can contain anything, but a set of registered claim names have agreed meanings. Getting these right is most of what “using JWTs correctly” means.

  • iss (issuer) — who created and signed this token. Your auth server’s identifier.
  • sub (subject) — who the token is about. A stable user ID, not an email; subjects should not change.
  • aud (audience) — who the token is for. A service should reject tokens whose audience isn’t it, or a token minted for one API can be replayed against another.
  • exp (expiration time) — after this moment, reject it.
  • nbf (not before) — before this moment, reject it.
  • iat (issued at) — when it was created. Handy for “re-authenticate if the session is older than X”.
  • jti (JWT ID) — a unique identifier for this token, so it can be tracked or denied individually.

Everything else is a custom claim: roles, tenant IDs, feature flags, whatever your application needs.

How expiry actually works

exp, iat, and nbf are NumericDate values: seconds since the Unix epoch, not milliseconds. This trips up JavaScript developers constantly, because Date.now() returns milliseconds — compare the two directly and your token appears to expire tens of thousands of years from now.

// exp is in seconds; Date.now() is in milliseconds
const isExpired = payload.exp * 1000 < Date.now();

Expiry is also evaluated by the receiver against its own clock. If two servers drift apart, valid tokens get rejected — which is why libraries usually allow a small leeway of a few seconds.

The part that matters most: decoding is not verifying

This deserves its own section, because misunderstanding it is how JWT systems get broken.

Decoding a JWT means Base64-decoding text that was never hidden. There is no secret involved. Anyone who obtains the token — a browser extension, someone reading a log file, a person looking over your shoulder — can read every claim in the payload.

Verifying is the separate step where the server recomputes the signature over the header and payload using its key, and checks that the result matches the third segment. Only that step proves the token came from your auth server and has not been tampered with.

Two consequences follow directly:

Never put secrets in a JWT payload. Not passwords, not API keys, not private notes, not anything you would not print on a postcard. A signed JWT is tamper-evident, not confidential. (There is a separate encrypted format, JWE, but the tokens you meet day to day are signed, not encrypted.)

Never trust a decoded payload on the server. If your backend reads role: "admin" out of a token without verifying the signature first, an attacker can simply edit the payload, re-encode it, and grant themselves admin. Decoding is for humans debugging; verification is for machines deciding.

Common pitfalls

The alg: none trap. The specification includes an “unsecured” mode with no signature at all. Libraries that honoured whatever alg the token requested could be handed a none token and would accept it. Modern libraries refuse this by default, but the lesson generalises: the server decides which algorithm is acceptable, not the token. The same reasoning defeats algorithm-confusion attacks, where a token signed with a public key as an HMAC secret is passed off as legitimate.

Storing them carelessly. A token in localStorage is readable by any JavaScript on your page, so one cross-site scripting bug hands over every session. An HttpOnly, Secure, SameSite cookie is generally the safer default, with its own CSRF considerations to handle.

Forgetting that they are hard to revoke. The appeal of JWTs is that a server can validate one without a database lookup — which also means it has no idea you clicked “log out”. Keep access tokens short-lived and pair them with refresh tokens, or maintain a deny-list keyed on jti.

Inspecting one safely

A token pasted into a random website is a token you have effectively disclosed. If it is a live session token, treat that as a leak: whoever runs the site now holds valid credentials until it expires.

So prefer tools that work locally. The JWT Decoder splits the segments, decodes the header and payload, and shows the expiry entirely in your browser — the token is never sent anywhere. In a terminal, splitting on dots and Base64url-decoding by hand works fine too. And if you want a feel for the primitives underneath, the HS256 in that header means HMAC using SHA-256, the same hash function you can experiment with in our Hash Generator.

Quick answers

Can I decode a JWT without the secret? Yes. The header and payload are encoded, not encrypted. The secret is only needed to verify the signature.

Why does every JWT start with eyJ? Because eyJ is the Base64 encoding of {" — the beginning of the JSON header.

Is it safe to put a user’s email in a JWT? It is readable by anyone holding the token, so treat it like data printed on a boarding pass rather than data in a database.

What is a sensible expiry? Short for access tokens — minutes, not days — with a refresh token handling longer sessions.

My token looks valid but keeps getting rejected. Check exp against server time, check aud matches the service, and check for clock drift between machines.

The takeaway

A JWT is three Base64url segments: metadata, claims, and a signature. Reading the first two is trivial and requires nothing secret, which makes JWT decoders excellent debugging tools and terrible hiding places. Put identity and authorisation data in the payload, keep secrets out of it, always verify before you trust — and remember that the signature, not the encoding, is what makes a token mean anything.

Tools mentioned in this guide

More developer guides

All guides
Developer What Is Base64 Encoding? A Complete Beginner's Guide Base64 turns binary data into safe, printable text. Learn how it works, when to use it, why it is not encryption, and how to encode or decode it in seconds. Developer URL Encoding Explained: Percent-Encoding, Query Strings and Common Bugs Why URLs break on spaces, ampersands and accents — and how percent-encoding fixes it. Covers encodeURI vs encodeURIComponent, + vs %20, and double encoding. Developer MD5 vs SHA-256: Which Hash Function Should You Use? MD5 is fast but broken; SHA-256 is the sensible default. Learn how hash functions work, why hashing is not encryption, and how to hash passwords properly.