Skip to main content
ToolMaple

JWT Decoder

Paste a JWT to see its header and payload, with the expiry turned into a readable date.

Last updated:

Decoded, no exp claim
No exp claim in the payload, so the lifetime is unknown

Time claims

  • iat (issued at)
    1516239022
    1/18/2018, 1:30:22 AM

Header

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

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Signature (base64url)

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

This page decodes only. It does not check the signature, which needs the secret or the public key.

What a JWT decoder shows you, and what it cannot

A JSON Web Token in its compact form is three chunks of base64url joined by dots: header.payload.signature. The first chunk says which algorithm signed the token and, usually, which key was used. The second is the set of claims: who the token is about, who issued it, who it is for, and when it stops being good. The third is the signature over the first two. This page reverses the base64url on the first two chunks, pretty-prints the JSON, and turns exp, iat and nbf into dates you can read.

The important word is decode. This page does not verify the signature and cannot tell you whether a token is genuine. Verification needs the shared secret for an HMAC algorithm such as HS256, or the issuer’s public key for RS256, ES256 or EdDSA, and a page that asked you for either would be asking for the wrong thing. What you get here is what the issuer claims, which is exactly what you need when you are reading a token, and nothing like enough to decide whether to trust it.

The quick version people write by hand, JSON.parse(atob(token.split('.')[1])), fails in two ways that are easy to miss. Base64url replaces + and / with - and _ and drops the = padding, so atob either throws or returns rubbish; and atob hands back one byte per character, so a name with an accent, an emoji or any Japanese or Korean text comes out mangled. This page fixes the alphabet, restores the padding, and decodes the bytes as UTF-8.

Which part of the token answers which question

  • Header, for key and algorithm problems: alg tells you how it was signed and kid names the key. When verification fails on the server, a kidthat is missing from the issuer’s key set is the first thing to look for.
  • Time claims, for “it worked five minutes ago”: the panel above converts exp, iat and nbf from Unix seconds into local dates, so you can see at a glance whether the token has run out or has not started yet.
  • iss and aud, for 401 responses from the right-looking token: a token minted by your staging tenant will not pass an audience check in production, and the two look identical until you read these two claims.
  • sub, for “which user is this?”: the subject is normally the stable user id. It is what you paste into a database query when a support ticket arrives with a token attached.
  • Scopes, roles and custom claims, for 403 responses: if the request is authenticated but refused, the permission the API wanted is usually missing from the payload rather than wrong on the server.
  • Signature, for almost nothing on its own: it is shown so you can confirm it exists and is not empty. An empty third segment means the token was issued with alg set to none, which no current library should accept.
  • When you need verification, not decoding: use a library on the server. In JavaScript the jose package handles both verification and remote key sets; in Python it is PyJWT; in Go, golang-jwt.

How to debug a 401 with the token in front of you

  1. Copy the token out of the Authorizationheader in your browser’s network panel, or out of the API client that made the call. Drop the leading Bearer and the space after it.
  2. Paste it above. If the decode fails, the problem is the copy rather than the token: look for a line break in the middle or percent-encoding left over from a URL.
  3. Read the status line. If it says expired, you are looking at a refresh problem, not an authentication problem, and the fix is in whatever is meant to renew the token.
  4. Compare iss and aud with what the API expects. Mismatched environments are the most common cause of a 401 on a token that decodes perfectly.
  5. Check alg and kidagainst the issuer’s published key set. A rotated key that your service has cached for too long produces a signature failure with no other symptom.
  6. Only then look at the payload for the scope or role the endpoint requires. A missing permission normally produces a 403, so if you are getting a 401 the cause is above this step.

How to read the expiry without doing the arithmetic

  1. Paste the token. The banner shows whether exp has passed and how long is left or how long ago it lapsed.
  2. Read the time panel for the raw Unix seconds next to the local date. If a number looks a thousand times too large, the issuer put milliseconds in a field that RFC 7519 defines as seconds, which breaks every standard verifier.
  3. Subtract iat from exp to get the lifetime. Fifteen minutes to an hour is typical for an access token; a token good for a year is a design decision worth questioning, because it cannot be withdrawn.
  4. If there is no exp at all, the tool says so rather than guessing. A token with no expiry is valid until the signing key is rotated.
  5. Remember that the verdict here uses your device clock. The server uses its own, which is why tokens sometimes fail a minute before or after you expect.

Getting the decoded claims into a ticket or a message

Select the payload block and copy it like any other text. It is formatted JSON, so it drops straight into a code fence in a pull request, an issue or a chat message. Before you send it anywhere, remember that the claims describe a real person: an email address, a user id, a tenant name and a role list are all common, and all of them are personal or commercially sensitive.

  • Share the claims, not the token. Paste the decoded JSON with the values you are discussing and replace the rest with placeholders. The token itself is a live credential; the decoded shape usually is not.
  • iPhone and iPad: press and hold on the block, drag the selection handles over the whole payload and tap Copy. Turning the phone to landscape stops the long lines wrapping into a maze.
  • Android: press and hold, then use the selection handles. Gboard keeps a clipboard history, so clear the entry afterwards if the token went through it.
  • Desktop: triple-click selects a line and click-drag selects the block. Ctrl+C on Windows and Linux, Cmd+C on a Mac.
  • Never paste a production token into a chat. Messages are searchable, backed up and often retained for years. If you must demonstrate a token, issue one against a test account or use one that has already expired.

How different issuers and languages shape the same token

Every JWT follows RFC 7519, but issuers differ in what they put beyond the registered claims. At the time of writing:

  • OpenID Connect providers issue an id_tokenthat is a JWT describing the sign-in, most often signed with RS256, alongside an access token whose format is not fixed by the spec. Their public keys and supported algorithms are published at the discovery document and key set URLs listed in the provider’s documentation.
  • Auth0 requires custom claims to be namespaced with a URI, so a role list appears under something like https://example.com/roles rather than a bare roles key. If a custom claim seems to vanish, an unnamespaced name is the usual reason.
  • Firebase Authentication ID tokens are signed with RS256. The sub is the Firebase UID, the aud is the project id, and a firebase object records how the user signed in.
  • Supabase access tokens carry the user id in sub and a role claim that Postgres row level security reads through auth.uid(). Older projects sign with a symmetric project secret; newer ones can use asymmetric keys, so check your project settings before assuming which.
  • Node: jsonwebtoken keeps decode and verify as separate functions on purpose. Reaching for decode in request handling code is the classic way to ship an authentication bypass.
  • Python: PyJWT uses one function for both, so jwt.decode(token, options={"verify_signature": False}) is the read-only form and anything else needs a key and an explicit algorithm list.
  • Shell: cut -d. -f2 <<< "$TOKEN" | base64 -d often fails, because base64url is a different alphabet and the padding has been stripped. Translate -_ back to +/ and add = signs until the length is a multiple of four.

Security, privacy and the limits of this page

Decoding is not verification. Nothing on this page checks a signature, so nothing here should ever be used to decide that a token is trustworthy. Never say a token is valid because a decoder rendered it. On the server, always verify with a library, always pass the algorithms you expect rather than trusting the alg field, and always check exp, iss and aud. The historic attacks on JWT come precisely from skipping those steps: a token re-signed with alg set to none, or an RS256 token replayed as HS256 so the public key is treated as a shared secret.

The payload is readable by anyone. Base64url hides nothing. Keep passwords, government id numbers, payment details and anything else you would not publish out of the claims. If the contents must be confidential in transit and at rest, that is what JWE is for, and a JWE has five segments rather than three.

A token cannot be taken back. Once issued, a JWT is accepted until it expires, because verifying it involves no lookup. Short lifetimes with a refresh token, a denylist of jti values, or a version number in the payload that you can bump are the three usual mitigations, and all three trade away some of the statelessness that made the format attractive.

What leaves your browser: nothing. The decode runs in this tab, and this site has no backend endpoint that could receive a token. That is a statement about this page, not about the rest of your workflow: the same token in a screenshot, a log file or a chat message is exposed regardless. When in doubt, decode an expired token, which has the same structure and none of the risk.

Frequently asked questions

Does this page verify the JWT signature?

No. This page does not verify signatures; it decodes the header and the payload and reads the time claims, nothing more. Verifying a signature needs the shared secret for an HMAC algorithm such as HS256, or the matching public key for RS256, ES256 or EdDSA, and neither belongs in a web page. A decoded token tells you what the issuer claims; only your server, after checking the signature and the expiry against its own clock, can decide whether to trust it.

Is it safe to paste a token here?

Decoding happens entirely in this browser tab. The token is never uploaded, because this site has no backend endpoint that could receive it. Even so, a live access token is a credential: anyone who gets it can act as the signed-in user until it expires. If you are debugging on a shared screen or in a recorded call, decode an already expired token instead. It has the same claim structure and is worthless to an attacker.

Why can anyone read the payload of my JWT?

Because base64url is an encoding, not encryption. The first two segments are plain JSON that anyone can reverse with one line of code. A signature protects integrity, so a tampered token is rejected, but it does not hide anything. If the contents genuinely need to be secret, you want JWE, the encrypted member of the JOSE family, which produces five dot-separated segments instead of three. Otherwise keep the payload free of anything you would not print on a postcard.

What do iss, sub, aud, exp, nbf, iat and jti mean?

They are the registered claims from RFC 7519. iss is the issuer, sub the subject the token is about (usually a user id), aud the intended audience or recipient, exp the expiry, nbf the earliest time the token is valid, iat when it was issued, and jti a unique id for the token. The three time claims are NumericDate values: seconds since the Unix epoch in UTC, not milliseconds.

The exp looks fine, so why does the API still return 401?

Expiry is one of several checks. The server also verifies the signature, that iss and aud match what it expects, that nbf has passed, and often that a scope or role claim is present. Clock skew is the other common cause: if the two machines disagree by a minute, a token can look fresh on your laptop and expired on the server. RFC 7519 lets implementations allow a small amount of leeway for exactly this reason.

The decoder says this is not a JWT. What should I check?

Count the dots. Three segments means a signed JWT, five means an encrypted JWE that cannot be decoded without the key. Then check for stray characters: a copied Authorization header often keeps the word Bearer and a space at the front, a token pasted from a log may have a line break in the middle, and one taken from a URL may still be percent-encoded, in which case decode the URL escaping first. Trailing quotes and commas from a JSON blob are the other usual suspects.

How do I decode a JWT in code or on the command line?

In Node, the jsonwebtoken package has decode for reading and verify for checking, and they are deliberately separate functions. In Python, PyJWT decodes without checking when you pass options with verify_signature set to false. In a browser, split on the dot, swap the base64url characters back, then pass the bytes through TextDecoder so non-ASCII values survive. On a shell, cut the second field and pipe it to base64, remembering that base64url uses different characters and usually drops the padding.

Can a JWT be revoked before it expires?

Not by itself, and that is the trade the format makes: the server does not look anything up, so it has nothing to invalidate. The usual workarounds are short-lived access tokens backed by a refresh token, a denylist of jti values checked on each request, or a token version number stored on the user record and copied into the payload, so bumping the number invalidates everything issued earlier. Each of these gives back some of the statelessness JWTs were chosen for.

Should I store a JWT in localStorage or in a cookie?

A cookie marked HttpOnly, Secure and SameSite cannot be read by JavaScript, so a cross-site scripting bug cannot steal it, but you then have to think about cross-site request forgery. localStorage is simpler to wire up and is what many single-page apps use, at the cost that any script running on your origin can read the token. If the account protects money, health data or admin access, prefer the cookie and add CSRF protection.

Why is my JWT so long, and does the length matter?

Every segment is base64url, which costs about a third more than the raw bytes, and the signature alone is at least 32 bytes for HS256 and considerably more for RSA. A few hundred characters is normal. Length matters because the token usually travels in a header or a cookie on every request: browsers commonly cap a single cookie at around 4 KB, and many servers reject request headers above a default of a few kilobytes. Large role or permission lists in the payload are the usual cause of a token that suddenly stops working.

Sources

  • IETF, RFC 7519, JSON Web Token (JWT) (the three-part compact form, the registered claims iss, sub, aud, exp, nbf, iat and jti, and NumericDate as seconds since the epoch).
  • IETF, RFC 7515, JSON Web Signature and RFC 7518, JSON Web Algorithms (the compact serialization and the HS, RS, PS and ES algorithm families, including the unsecured none algorithm).
  • IETF, RFC 4648, The Base16, Base32, and Base64 Data Encodings (section 5 defines the base64url alphabet used by every JWT segment).
  • MDN, TextDecoder (why decoded bytes have to be read as UTF-8 rather than taken straight from atob).
  • Claim layouts for individual identity providers come from each vendor’s own documentation and can change without notice. Check your provider’s current reference before relying on a specific custom claim.

More developer tools

To decode a single segment by hand, or any other base64url string, use the Base64 encoder and decoder. To tidy a payload you have copied out, or to check that an API response parses at all, the JSON formatter validates and pretty-prints it. A token pulled out of a query string may still be percent-encoded, which the URL encoder and decoder reverses, and if you need a fresh identifier for a jti claim or a test account, the UUID generator makes one.

Related tools

Privacy: the token is decoded in your browser and never sent to a server, stored or logged. This site has no API endpoint that could receive it, and nothing you paste is included in analytics. Closing the tab discards it.