What a JWT actually is
A JSON Web Token is a compact, self-contained way to carry a set of statements — called
claims — between two parties. Written out, it is three chunks of Base64url text joined by dots:
header.payload.signature. The header is a small JSON object naming the signing algorithm and
the token type. The payload is another JSON object holding the claims: who the token is about, who issued
it, when it expires, and whatever application-specific data the issuer chose to include. The signature is
a cryptographic value computed over the first two segments together with a key.
The point of that structure is that a recipient can validate the token without a database lookup. If the signature checks out against the expected key, the payload has not been altered since it was issued, so the server can trust the user ID and permissions inside it and get on with the request. That property is what makes JWTs so common in stateless APIs and single sign-on flows — and it is also the source of every one of their downsides, because a statement you can verify offline is a statement you cannot easily retract.
Decoding is not verifying — and it never will be
This is the single most important thing to understand about any online JWT decoder, including this one.
Reading a token requires nothing but a Base64 decoder. Verifying it requires a secret. For an
HS256 token the verifier needs the shared HMAC secret; for RS256 or
ES256 it needs the issuer's public key, usually fetched from a JWKS endpoint. A browser page
has none of that, so this tool deliberately makes no claim about authenticity. A token with a completely
fabricated signature will decode and display exactly like a genuine one. Treat what you see here as
"what this token says", never as "what this token proves".
There is a second reason this page will never offer to verify: doing so would mean asking you to paste your signing secret into a web form. No online tool should ask for that, and you should not give it to one. Verification belongs in your own server process, using a library that you have configured with the exact algorithm and key you expect.
A JWT payload is not encrypted — here is the proof
Base64url is an encoding. It exists to make arbitrary bytes safe to put in a URL, not to hide them. There is no key involved, so reversing it takes no privilege whatsoever. Consider this payload:
{"sub":"user_10923","email":"ada@example.com","password":"hunter2","role":"admin"}
Base64url-encoded, that becomes the middle segment of a token, and it looks satisfyingly opaque:
eyJzdWIiOiJ1c2VyXzEwOTIzIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5jb20iLCJwYXNzd29yZCI6Imh1bnRlcjIiLCJyb2xlIjoiYWRtaW4ifQ
It is not opaque at all. Paste that string into the Base64 decoder in
URL-safe mode and the original JSON comes straight back, password and all — no key, no secret, no
cracking. Anyone who can see the token can see the payload: the user's browser, any browser extension,
anything reading the Authorization header, a proxy log, an error report, a screenshot in a
support ticket. The signature stops the payload being changed. It does nothing to stop it being
read.
So the rule is simple: a JWT payload is public data. Passwords, full card numbers, government identifiers, internal API keys, private notes and anything you would not print on a postcard do not belong in it. Put an opaque user ID and the minimum set of claims your API needs to authorise the request. If you genuinely need confidentiality rather than integrity, the JWE encrypted-token format exists for exactly that, and it is a different thing from the signed JWS tokens almost everyone means when they say "JWT".
Every claim, explained
Registered payload claims
These seven are defined by RFC 7519. All are optional, all are three letters, and every one of them is a thing your verifier should be checking rather than merely displaying.
| Claim | Name | Type | What it means and what to check |
|---|---|---|---|
iss | Issuer | string | Who created and signed the token, usually the auth server's URL. Pin the exact expected value — accepting any issuer means accepting tokens minted by anyone whose key you happen to trust. |
sub | Subject | string | Who the token is about, typically an opaque user ID. Unique within the issuer, not globally, so key your users on the iss and sub pair rather than sub alone. |
aud | Audience | string or array | Which API the token is intended for. Reject tokens addressed elsewhere — otherwise a token issued for a low-trust service can be replayed against a high-trust one. |
exp | Expiration | number (seconds) | Unix time after which the token must be rejected. Required in practice even though the spec makes it optional; a token without exp is valid forever. |
nbf | Not before | number (seconds) | Unix time before which the token must be rejected. Used for tokens issued ahead of time; a common cause of confusing failures when clocks drift. |
iat | Issued at | number (seconds) | When the token was created. Useful for enforcing a maximum age independently of exp, and for detecting tokens issued before a credential change. |
jti | JWT ID | string | A unique identifier for this token, conventionally a UUID. This is the handle a denylist uses to revoke one specific token, and the key a replay check deduplicates on. |
All the time-based claims are seconds since the Unix epoch, not milliseconds. Mixing the
two up is a classic bug: a millisecond exp value lands roughly fifty thousand years in the
future, so the token effectively never expires and no error is ever raised to tell you. The decoder above
renders every timestamp claim in both UTC and your local timezone precisely so this stands out.
Common non-standard claims
Beyond the registered seven, identity providers add their own. These are the ones you will meet most often — but check your provider's documentation, because none of them are guaranteed.
| Claim | Usually | Notes |
|---|---|---|
scope | Space-separated string | OAuth 2.0 scopes such as read:orders write:orders. Note the singular name and the space delimiter — scopes as an array is a common provider-specific variation, and mixing them up produces authorisation checks that silently always fail. |
roles | Array of strings | Application roles. Entirely non-standard; Auth0 namespaces it, Keycloak nests it under realm_access.roles, Azure AD uses roles at the top level. Never assume the shape. |
email | String | From OpenID Connect, and only meaningful together with email_verified. Treating an unverified email claim as an account identifier is a well-known account-takeover path. |
azp | String | Authorized party — the OAuth client ID the token was issued to, as distinct from the audience it is addressed to. Check it when one API serves several client applications with different privileges. |
client_id | String | The OAuth client, in providers that follow RFC 9068 for access tokens rather than using azp. |
auth_time | Number (seconds) | When the user actually authenticated, which can be much earlier than iat if the session was silently refreshed. Check it before allowing sensitive operations. |
nonce | String | Echoes the value your client sent in an OpenID Connect authorization request, binding the ID token to that specific login attempt. |
Header parameters
| Parameter | Meaning |
|---|---|
alg | The signing algorithm, for example HS256, RS256 or ES256. Informational only — your verifier must pin the algorithm it expects rather than obeying this field. |
typ | Media type of the token, almost always JWT. RFC 9068 recommends at+jwt for OAuth access tokens so they cannot be confused with ID tokens. |
kid | Key ID — tells the verifier which key from the issuer's JWKS to use. Essential for key rotation. Never fetch a key from a URL supplied inside the token itself. |
cty | Content type of the payload, used when a JWT is nested inside another JWT. |
jku / x5u | URLs pointing at the key set or certificate. Seeing these in a token you receive is a red flag: a verifier that follows them can be pointed at an attacker's key. |
Algorithms: HS256 vs RS256 vs ES256
The alg value in the header names how the signature was produced. The three you will meet
almost every time split into two very different families.
| Algorithm | Family | Key material | Signature size | Use when |
|---|---|---|---|---|
HS256 | Symmetric — HMAC-SHA256 | One shared secret, ≥256 bits | 32 bytes | The same party issues and consumes the token |
RS256 | Asymmetric — RSASSA-PKCS1-v1_5 + SHA-256 | RSA private/public pair, ≥2048-bit | 256 bytes | Many verifiers, broad library support required |
PS256 | Asymmetric — RSASSA-PSS + SHA-256 | Same RSA pair | 256 bytes | You want RSA with the modern, provably-secure padding |
ES256 | Asymmetric — ECDSA on P-256 + SHA-256 | EC private/public pair | 64 bytes | Size matters and your stack supports EC keys |
EdDSA | Asymmetric — Ed25519 | Ed25519 pair | 64 bytes | Modern greenfield systems; fastest and hardest to misuse |
none | No signature at all | None | 0 bytes | Never, in anything that faces a network |
Symmetric: one secret does both jobs
HS256 computes an HMAC over header.payload using a shared secret. It is fast,
the signature is small, and every language has it built in. The catch is structural: the key that
verifies is the key that signs. Any service you give the secret to so it can validate tokens can
also mint tokens claiming to be anyone. In a monolith where one process issues and consumes its own
session tokens, that is a non-issue and HS256 is a perfectly good choice. In a microservice estate, it
means distributing a forging key to a dozen deployments, which is exactly the situation asymmetric
signing was invented to avoid.
A second HS256 hazard is key strength. The secret is just a byte string, so nothing stops someone using
secret or the application name. An HMAC signature is offline-crackable: an attacker with one
captured token can grind candidate secrets on a GPU until the signature matches, then forge tokens
forever. Use at least 256 bits from a cryptographic random source — the
UUID generator on this site is a convenient way to get random bytes
if you have nothing better to hand, though a dedicated secret manager is the right long-term answer.
Asymmetric: the verifier cannot forge
RS256 and ES256 sign with a private key held only by the issuer and verify with
a public key that can be published to the world. This is what makes federated identity work: your API
downloads the identity provider's JWKS document, caches it, and validates tokens forever without ever
holding anything sensitive. A compromised resource server leaks no ability to issue tokens.
Between the two, ES256 gives equivalent security with dramatically smaller keys and
signatures — 64 bytes against RSA's 256. That matters when tokens travel in headers on every request,
because a fat token eats into proxy header limits and can turn into a
431 Request Header Fields Too Large. RS256 remains the
most widely supported and is still the sensible default when you cannot control every consumer's library.
PS256 and EdDSA are better cryptography; use them when your whole stack agrees.
Why alg: none is an attack class, not a curiosity
The JWT specification defines none as a valid algorithm for unsecured tokens, and the header
that declares it is supplied by whoever holds the token. That combination is the flaw. An attacker takes
a real token, rewrites the payload to {"sub":"admin"}, sets the header to
{"alg":"none"}, drops the signature segment and leaves a trailing dot. A library that reads
the algorithm out of the token and dispatches on it will conclude that no signature is required and
accept the forgery. This was not hypothetical: it was found in a long list of popular JWT libraries in
2015 and it still resurfaces.
The closely related algorithm confusion attack targets asymmetric setups. The attacker
takes a server's public RSA key — which is public by design — rewrites the header from
RS256 to HS256, and signs the forged token using that public key as the HMAC
secret. A verifier that trusts the header calls the HMAC path with the RSA public key as the secret, the
signature matches, and the token is accepted.
Both are fixed the same way, and it is the single most important line of code in JWT handling:
pin the expected algorithm in the verification call. Pass
algorithms: ['RS256'] — an explicit allowlist — rather than letting the library infer it.
Reject none unconditionally. If the decoder above ever shows you alg: none in a
token you received in production, treat it as an incident, not a quirk; it flags this case explicitly for
that reason.
Using JWTs safely
Keep the payload public-safe and small
Covered above, but worth restating as a rule: assume every field in the payload will be read by someone you did not intend. Beyond secrecy there is a size argument. Tokens travel in the Authorization header on every single request, and many proxies cap total header size at 4–8 KB. Serialising a whole user profile, a permission matrix or an avatar URL into the payload produces a token that works in development and fails behind the load balancer with a 431 or an opaque 400. Carry an identifier and let the API look up the rest.
Token lifetime
Lifetime is your primary damage-control lever, because it is the one thing that limits how long a stolen token is useful. The conventional split:
- Access tokens: 5 to 15 minutes. Short enough that a leaked token expires before most attackers act on it, long enough that refreshes are not constant.
- ID tokens: minutes. They are consumed once at login to establish a session, not presented repeatedly.
- Refresh tokens: days to weeks, stored server-side and revocable, ideally with rotation — each use issues a new refresh token and invalidates the old one, so a replayed refresh token is detectable evidence of theft and can kill the whole family.
- Never "no expiry". A token without
expis a permanent credential you have no way to withdraw.
Allow a small clock skew — 30 to 60 seconds is typical — when checking exp and
nbf. Servers whose clocks are a few seconds apart otherwise reject perfectly good tokens,
and the resulting 401s are miserable to diagnose.
Storage: localStorage vs httpOnly cookie
There is no option here that is safe against everything; there is a trade you have to make consciously.
| httpOnly cookie | localStorage / sessionStorage | |
|---|---|---|
| Readable by JavaScript | No | Yes |
| Exposure to XSS | Token cannot be exfiltrated, but the attacker can still make authenticated requests from the page | Token is read and exfiltrated in one line |
| Exposure to CSRF | Yes — needs SameSite and usually a CSRF token | No — nothing is sent automatically |
| Cross-origin API calls | Awkward: needs CORS credentials and SameSite=None; Secure | Easy: attach the header yourself |
| Survives a page reload | Yes | Yes (localStorage) / no (sessionStorage) |
| Mobile and native clients | Poor fit | Use the platform keychain instead |
For a browser application talking to its own backend, an httpOnly; Secure; SameSite=Lax
cookie is the better default: it removes the token-theft class of XSS outcome entirely, and CSRF is a
well-understood problem with a mechanical fix. For a single-page app calling an API on a different
origin, or any non-browser client, header-based tokens are the pragmatic choice — pair them with a strict
Content Security Policy and keep the lifetime short. And note the honest caveat in the table: if an
attacker has script execution on your page, they can act as the user either way. Fixing XSS is not
optional groundwork for this decision, it is the decision that matters most.
Revocation is the hard problem
The whole appeal of a JWT is that verification needs no shared state. The direct consequence is that
there is no state to change when you want a token to stop working. Logging a user out clears their
browser; it does nothing to a copy of the token an attacker already holds. Deleting the user account does
not stop a token bearing their ID from validating until exp. Changing a password does not
either, unless you build something that makes it.
The workable approaches, roughly in order of cost:
- Short lifetimes. Not revocation, but it bounds the window to minutes and covers most real scenarios. This is why it is the first line of defence rather than an optimisation.
- A
jtidenylist. Store revoked token IDs in a fast cache with a TTL matching the token's remaining life, and check it on every request. Cheap because the list is naturally small, but it reintroduces the stateful lookup JWTs were meant to remove. - A per-user token version. Keep an integer on the user record, include it as a claim, and increment it on password change or forced logout. One lookup per request, but it invalidates every token for that user at once — the right tool for "log me out everywhere".
- Refresh token rotation. Keep access tokens short and un-revocable, and make refresh tokens stateful, single-use and revocable. Reuse of a rotated refresh token is a strong theft signal.
- Opaque tokens with introspection. If you need immediate revocation on every request, a random token checked against a store is the honest design. Accepting that a JWT is the wrong tool is sometimes the correct engineering answer.
Whichever you pick, decide it before launch. Retrofitting revocation onto a system with twenty-four-hour access tokens during an active incident is not a good day.
Troubleshooting: what the failure means
Most JWT problems reduce to a small set of causes. This table maps the symptom to the check, and to the response your API should be returning.
| Symptom | Likely cause | Correct response |
|---|---|---|
| Token decodes fine, API rejects it | exp has passed, or the clock is skewed |
401 with WWW-Authenticate: Bearer error="invalid_token" |
| Works against one service, fails on another | aud does not match the second service |
401 Unauthorized |
| "Invalid signature" from the library | Wrong key, wrong kid, or a rotated JWKS not refetched |
401 Unauthorized |
| Works in staging, fails in production | iss points at the staging authorization server |
401 Unauthorized |
| Authenticates, then still refused | Identity is fine; the required scope or role is absent |
403 Forbidden — do not use 401 here |
| "Malformed token" / segment count error | The Bearer prefix or a line break was included |
400 Bad Request |
| Token rejected as not yet valid | nbf is in the future relative to the server clock |
401 Unauthorized |
| Request fails before reaching your code | Token grew too large for the proxy's header limit | 431 Request Header Fields Too Large |
The 401-versus-403 distinction is the one teams get wrong most often, and it matters because clients behave differently: a 401 should prompt a refresh-and-retry, while a 403 should not, because retrying with the same identity will never succeed. Use 401 when you do not know who the caller is — no token, expired token, bad signature — and 403 when you know exactly who they are and they are not allowed. Reproduce the failing call on the API tester and paste the same token in here to compare what the token says with what the API expected.
When you reach for a JWT decoder
Debugging a 401 that should have worked
You send a request, the API answers 401 Unauthorized, and the token
looks fine. Decoding it usually settles the question in seconds: the exp has passed, the
aud points at a different service, the iss is your staging auth server rather
than production, or a required scope is simply absent from the payload.
Checking what a third party actually issued
When you integrate an identity provider, the claim names in the documentation and the claim names in the
real token do not always agree. Decoding a live token tells you what you are actually receiving —
including custom namespaced claims that the docs often omit entirely, and the exact shape of
roles or scope that your authorisation check has to parse.
Confirming your own token minting
If you issue tokens, decoding one after a deploy is a fast sanity check that the algorithm in the header
is what you intended, that kid is present when you rotate keys, that exp is in
seconds and the lifetime is what you configured, and that you have not accidentally serialised a whole
user record into the payload. Pretty-print the result with the
JSON formatter if you want to diff two tokens side by side.
Common pitfalls
- Copying the
Authorizationheader, not the token. The header value isBearer eyJ.... The wordBearerand the space are not part of the JWT. This decoder strips a leadingBearerfor you, but many libraries will not. - Whitespace and line breaks. Tokens copied out of logs or terminals often arrive wrapped. Any stray whitespace inside the token breaks Base64 decoding.
- Confusing Base64 with Base64url. JWT segments use
-and_in place of+and/, and drop the trailing=padding. A plain Base64 decoder may reject a segment that is perfectly valid; use its URL-safe mode. - Clock skew. Expiry here is judged against your own machine's clock. A workstation several minutes out of sync will disagree with the server.
- Trusting
algfrom the token. A server must pin the expected algorithm rather than reading it from the header, or an attacker can presentalg: noneor swapRS256forHS256and skip verification entirely. - Not validating
audandiss. A signature check alone only proves the token came from a key you trust — not that it was meant for you. Both must be pinned. - Assuming revocation. A signed, unexpired token stays valid until it expires. Logging a user out does not invalidate a JWT unless you maintain a denylist or token version.
- Putting a JWT in a URL. Query strings land in server logs, browser history and
Refererheaders. Tokens belong in a header or an httpOnly cookie — see the HTTP headers guide.
Frequently asked questions
Does this decoder verify the signature?
No. Signature verification needs the HMAC secret or the issuer's public key, which a browser tool has no safe way to hold — and no online tool should ever ask you for your signing secret. This page decodes the header and payload only. Verify on the server, and pin the algorithm you expect rather than reading it from the token.
Is my token uploaded anywhere?
No. Everything happens in JavaScript on this page. Open your browser's network tab while decoding — there are no requests. The token is not logged, stored or transmitted.
Is a JWT encrypted?
A standard signed JWT is not. Base64url is reversible by anyone with no key at all — paste the middle segment into a Base64 decoder and you get the JSON straight back. The signature protects the payload from tampering, not from being read. Keep secrets out of the payload; use JWE if you need real confidentiality.
What is the difference between HS256, RS256 and ES256?
HS256 is symmetric: one shared secret both signs and verifies, so every verifier can also forge. RS256 and ES256 are asymmetric: a private key signs, a public key verifies, so a resource server can validate without being able to issue. ES256 gives equivalent strength to RS256 with a 64-byte signature instead of 256 bytes.
Why is alg: none dangerous?
Because the algorithm is declared by whoever holds the token. A library that dispatches on the header
will accept a token that says none with an empty signature, skipping verification entirely.
The variant attack rewrites RS256 to HS256 and signs with the public key. Both
are fixed by passing an explicit algorithm allowlist to your verifier.
Why does the tool say expired when my API accepts the token?
Expiry is evaluated against your device clock. Servers also commonly allow a small skew allowance of
thirty to sixty seconds. Check your system time, and check that exp is in seconds rather
than milliseconds, before concluding the token is at fault.
Should I store a JWT in localStorage or a cookie?
An httpOnly, Secure, SameSite cookie is the safer default because script cannot read it, so XSS cannot exfiltrate the token — at the cost of needing CSRF protection. localStorage avoids CSRF and suits cross-origin APIs but is readable by any injected script. If you have XSS, neither saves you; fix that first.
How do I revoke a JWT?
Mostly you cannot, which is the format's central trade-off. Use short access-token lifetimes with
revocable rotating refresh tokens, a denylist keyed on jti, or a per-user token version you
bump on password change. If you need instant revocation on every request, an opaque token checked against
a store is the more honest design.
Can I decode a token that has no signature segment?
Yes. Unsigned tokens use alg: none and end with a trailing dot and an empty third segment.
They decode fine here and the tool flags them, but no production API should ever accept one.
Related tools and reference
API Tester
Replay the failing request with the same bearer token and read the actual response.
Base64 Encoder & Decoder
Decode individual JWT segments by hand, or build Basic auth header values.
JSON Formatter
Pretty-print a large decoded payload and validate it before pasting it elsewhere.
UUID Generator
Generate the jti claim, or random bytes for an HMAC signing secret.
HTTP Headers Guide
How the Authorization header carries a bearer token, and what else travels with it.
401 Unauthorized
What a 401 means and the checklist for working out which part of auth failed.
403 Forbidden
The right answer when the token is valid but the scope or role is missing.
REST API Testing Guide
Where token handling fits into a repeatable API testing workflow.