Skip to content

JWT Decoder

A JSON Web Token is three base64url-encoded segments joined by dots: header, payload, and signature. This decoder splits and decodes the first two in your browser, turns exp, iat and nbf into real dates, and flags expiry. It does not verify the signature — that needs the issuer's key, which this page never has.

A leading Bearer is stripped automatically. Decoding happens in your browser with JavaScript — the token is never sent to ProbeDNS or anywhere else.

This tool does not verify the signature. A JWT payload is base64url, not encryption: anyone holding the token can read every claim in it, which is exactly what is happening here. Whether the token is authentic depends on checking the signature against the issuer’s secret (HMAC) or public key (RSA/ECDSA), and this page has neither. Treat everything below as “what the token claims”, never as “what is true”. And because the payload is readable, never put a password, a card number, or anything else confidential in one.

Paste a token above to see its header and claims.

Decoding is not verification

This distinction is the whole reason to read past the first paragraph. Decoding a JWT tells you what the token *claims*. Verifying it tells you whether those claims came from the issuer you trust and have not been altered. They are entirely separate operations, and only the second one requires a secret.

The payload is base64url, not encryption. Anyone holding the token — including any proxy, any log aggregator that captured the Authorization header, and anyone with access to the browser it is stored in — can read every claim in it. That is exactly what is happening on this page. The consequence for your own systems is simple: never put anything confidential in a JWT payload. No passwords, no card numbers, no internal database rows you would not show the user. If the token needs to carry secrets, you want JWE (an encrypted token), not JWS.

Because this page cannot verify anything, treat everything it shows as unverified assertions. A token showing "admin": true proves nothing — anyone can craft that payload. Only a signature check against the issuer's key makes a claim trustworthy.

The three segments

A signed token (JWS) has the form header.payload.signature. The header is a JSON object declaring the algorithm (alg) and type (typ), and often a key ID (kid) telling the verifier which of the issuer's keys was used. The payload is a JSON object of claims. The signature covers the base64url encoding of the first two segments joined by a dot — which is why you cannot change a single character of the payload without invalidating it.

Read the full explanation of jwt decoder results ↓

If you paste something with five dot-separated segments, that is a JWE — an encrypted token whose payload cannot be read at all without the decryption key. This tool will tell you so rather than producing nonsense. Two segments with an empty third means an unsigned token, which is a red flag on anything that matters.

The registered claims

ClaimNameWhat it means
issIssuerWho minted the token. A verifier must check this, not just the signature.
subSubjectWho the token is about — usually a stable user ID, not an email.
audAudienceWho the token is *for*. Rejecting a token meant for a different service is a required check.
expExpirationNumericDate after which the token must be rejected.
nbfNot beforeNumericDate before which the token must be rejected.
iatIssued atWhen it was minted. Useful for detecting a token older than a policy allows.
jtiJWT IDUnique identifier, used for replay detection and revocation lists.

All three time claims are NumericDate: seconds since 1970-01-01 UTC, not milliseconds. This trips people up constantly, because JavaScript's Date.now() returns milliseconds. Signing a token with exp: Date.now() + 3600000 produces a token that expires in about the year 56,000; signing with a millisecond value where seconds are expected fails validation immediately on some libraries and passes absurdly on others. This decoder interprets them as seconds, per RFC 7519 §2, and shows both the local time and the ISO form so a wrong-by-1000 value is immediately obvious.

A missing exp deserves attention. A token with no expiry is valid forever unless the issuer maintains a revocation list — and the main appeal of JWTs is that they are stateless, meaning most issuers do not. That combination is how a leaked token stays useful for years.

Reading the alg field

HS256 is HMAC-SHA-256: one shared secret both signs and verifies. Fine when a single service issues and consumes its own tokens; unworkable when you need to distribute verification, because every verifier would also be able to mint tokens. RS256 and ES256 are asymmetric — the issuer signs with a private key and anyone can verify with the public key, which is what OIDC providers publish at a JWKS endpoint.

alg: "none" means the token asserts it is unsigned. It exists in the specification for tokens whose integrity is guaranteed by other means, and it has been the basis of a long series of real vulnerabilities: a library that reads the algorithm from the header and dispatches on it will happily "verify" a none token that an attacker rewrote. The related attack swaps RS256 for HS256 and signs with the *public* key as the HMAC secret. The defence for both is the same — the verifier must pin the algorithm it expects and refuse anything else, never trust the header to choose.

This tool flags alg: none explicitly because seeing it in a token from a production system is worth investigating immediately.

Verifying properly, on the command line

Decoding is a one-liner. Verification needs the key, and is worth doing with a real library rather than by hand — but the manual HMAC check below is instructive because it shows exactly what the signature covers.

JWT='eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature'

# Decode the header and payload (base64url -> base64, then decode)
echo "$JWT" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
echo "$JWT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

# Is it expired? Compare exp against now, in seconds
echo "$JWT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null \
  | jq --argjson now "$(date +%s)" '{exp, now: $now, expired: (.exp < $now)}'

# Verify an HS256 signature by hand — the input is header.payload, verbatim
SIGNING_INPUT=$(echo "$JWT" | cut -d. -f1,2)
printf '%s' "$SIGNING_INPUT" \
  | openssl dgst -binary -sha256 -hmac 'your-shared-secret' \
  | openssl base64 -A | tr '+/' '-_' | tr -d '='
# Compare that to: echo "$JWT" | cut -d. -f3

# Fetch an issuer's public keys for RS256/ES256 verification
curl -s https://accounts.google.com/.well-known/openid-configuration | jq -r .jwks_uri
curl -s "$(curl -s https://accounts.google.com/.well-known/openid-configuration | jq -r .jwks_uri)" | jq .
Decode locally, then verify with the key

Note that the signing input is the raw header.payload string exactly as it appears in the token — not a re-encoding of the decoded JSON. Re-serialising the payload changes key order and whitespace and produces a different signature, which is why verification must operate on the original bytes.

A verifier's checklist

  • Pin the expected algorithm. Never dispatch on the token's own alg header.
  • Verify the signature before reading any claim. A payload from an unverified token is attacker-controlled input.
  • Check exp and nbf, with a small clock-skew allowance (60 seconds is conventional).
  • Check iss against the exact issuer you expect, and aud against your own service identifier. A valid token for a different audience is not a valid token for you.
  • For asymmetric tokens, fetch keys from the issuer's JWKS endpoint and match on kid. Cache them, and handle key rotation — a hardcoded public key becomes an outage on rotation day.
  • Keep token lifetimes short. Statelessness means you generally cannot revoke, so expiry is your only real control.
  • Store tokens where XSS cannot read them where possible. localStorage is readable by any script on the page; an HttpOnly cookie is not.

If a token in your hands is failing verification and you cannot see why, decode it here first. Roughly half of all JWT bugs turn out to be visible in the payload alone: a millisecond exp, an aud naming a different environment, an iss pointing at the staging tenant, or a kid referring to a key that was rotated out. The other half are clock skew and a mismatched secret.

JSON Formatter
Pretty-print, minify, and validate JSON

Frequently asked questions

Does this tool verify the signature?
No, and it cannot. Verification requires the issuer's HMAC secret or public key, which this page has no access to. It decodes the header and payload so you can read them, and tells you what the token claims — not whether those claims are authentic.
Is my token sent to your server?
No. Splitting and base64url decoding happen in your browser. No request is made — you can confirm this in the devtools Network tab. That matters, because a JWT pasted into a server-side decoder is a live credential handed to a third party.
Is a JWT encrypted?
A standard signed JWT (JWS) is not — the payload is base64url and readable by anyone. Only a JWE, which has five dot-separated segments instead of three, is encrypted. Never put confidential data in a signed JWT.
Why does my token show an expiry in the year 56,000?
The exp claim was set in milliseconds instead of seconds. RFC 7519 NumericDate is seconds since the epoch. If you built it in JavaScript, use Math.floor(Date.now() / 1000) + lifetime.
What does alg: none mean?
The token asserts it is unsigned. Any system that accepts it is vulnerable to trivially forged tokens, because a library that reads the algorithm from the header can be told not to check anything. Verifiers must pin the algorithm they expect and reject the rest.
Can I edit a JWT payload and still have it work?
Not against a verifier that checks the signature — the signature covers the exact header and payload bytes, so any change invalidates it. It will work against a system that decodes without verifying, which is precisely the bug this page exists to remind you about.
Where should I store a JWT in a browser?
An HttpOnly, Secure, SameSite cookie is safest, because JavaScript cannot read it and XSS therefore cannot exfiltrate it. localStorage is convenient and readable by any script on the page, including an injected one. Whichever you choose, keep lifetimes short.
How do I revoke a JWT?
Fundamentally, you cannot — statelessness is the point, and a verifier that only checks the signature has no way to learn a token was withdrawn. In practice you keep expiry short, pair access tokens with revocable refresh tokens, and maintain a denylist keyed on jti for emergencies, which reintroduces the state JWTs were meant to avoid.