How to Decode JWT Safely (Why Online Privacy Matters)

Mirsal Saidu Updated 8 min read

Decode JWT tokens safely in your browser: read the header, payload and expiry without sending the token anywhere, and learn why decoding is not verification.

How to Decode JWT Safely (Why Online Privacy Matters)

Before you decode a JWT, check where the decoding happens. Never paste a production token into a site you don't trust. A JSON Web Token is usually a bearer credential: whoever holds it can use it until it expires, so a decoder that logs tokens can act as you. I built our JWT decoder to run entirely in the browser, so we never see your token. This guide shows what decoding reveals, what it can't tell you, and how to inspect a token without leaking it.

How do I decode a JWT safely?

Decode JWT tokens locally. A JWT's first two parts are just base64url-encoded JSON, so decoding needs no key and no server. Our JWT Decoder decodes with JavaScript in your browser and shows the header, the payload, the signature as hex and the token's time claims without sending the token anywhere, which you can confirm in the Network tab of your browser's DevTools. Decoding does not check the signature, so it tells you what a token claims, not whether the claims are genuine.

What's inside a JWT

Here is an example token, signed with HS256 and the secret demo-secret-do-not-use. It has three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzQyIiwibmFtZSI6IkFkYSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyOTIwMH0
.zCGUFR6z0eEjPV8Paaj1OSBJdHCSbLFg64JUizPGGYo

(The line breaks are only for display; a real token is one unbroken string.) RFC 7515 defines this compact form as the base64url-encoded header, the base64url-encoded payload and the base64url-encoded signature, joined by dots. Decoding the first two parts gives plain JSON:

Header:    {"alg":"HS256","typ":"JWT"}
Payload:   {"sub":"user_42","name":"Ada","role":"editor","iat":1767225600,"exp":1767229200}
Signature: cc2194151eb3d1e1233d5f0f69a8f53920497470926cb160eb82548b33c6198a

The signature is 32 bytes, shown as 64 hex characters, which is the size of an HMAC-SHA256 result. JWT base64url drops the = padding (RFC 7515, section 2), so a standard decoder can fail on it. Python's base64.urlsafe_b64decode() raises binascii.Error: Incorrect padding on the payload above until you add one =. Our decoder adds the padding itself. For more on the encoding, see what is Base64 encoding.

Decoding is not verifying

Change "role":"editor" in the example to "role":"admin", re-encode the payload and keep the original signature. The decoder displays the edited token without complaint, with "role": "admin" in the payload. Verification with the secret catches it: the correct signature for the edited payload would be Oi32FCKdGa1zcFJC-2qVDHc0azlvAxLVgrplNMwpeZw, the token still carries zCGUFR6z…, and the check fails.

DecodingVerifying
Needs the secret or public keyNoYes
Shows the header and claimsYesYes
Detects an edited payloadNoYes
Proves who issued the tokenNoYes, if the key belongs to the issuer
Where it should happenAnywhere, ideally locallyOn your server, before trusting any claim

RFC 7519 puts it plainly: a JWT's contents "cannot be relied upon in a trust decision unless its contents have been cryptographically secured". Also watch for "alg":"none" in a header. RFC 7519 defines that as an unsecured JWT with no signature, and RFC 8725 describes attacks in which a token's algorithm was changed to none and some libraries accepted it without checking any signature.

How to decode JWT tokens with our JWT decoder

  1. Where you can, use a token from a test environment or one that has already expired.
  2. Open the JWT decoder. If you want proof that nothing is sent, open DevTools, select the Network tab, and keep it visible.
  3. Paste only the token, not the Bearer prefix from an Authorization header; the space and the word "Bearer" aren't valid base64url, and decoding fails. The tool decodes automatically once the input has three dot-separated parts, or you can click Decode JWT.
  4. Read the Header (JOSE), Payload (Claims) and Signature panels. Copy Header and Copy Payload copy the formatted JSON.
  5. Check the Token Timing cards and the status line, then click Clear.

Two errors are common. A token with five parts produces ✕ Invalid JWT: Token must have 3 parts separated by dots (header.payload.signature). Found 5 parts. That is an encrypted JWE token, which RFC 7516 writes in five parts; its payload can't be read without the decryption key. A token that was cut off while copying usually fails with ✕ Failed to decode JWT: followed by a JSON error, because the payload no longer ends in a closing brace.

The signature panel shows raw bytes as hex, which is easier to compare by eye than Base64; Base64 vs hex encoding covers the difference. To decode a single part by hand, the Base64 encoder and decoder also runs in your browser, and its Decode tab accepts the URL-safe - and _ characters that JWTs use.

How to decode JWT locally from the command line

You can also skip the browser and use a terminal. Each command below prints {"sub":"user_42","name":"Ada","role":"editor","iat":1767225600,"exp":1767229200} for the example token (jq prints it indented):

TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwibmFtZSI6IkFkYSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyOTIwMH0.zCGUFR6z0eEjPV8Paaj1OSBJdHCSbLFg64JUizPGGYo'

# jq 1.7
echo "$TOKEN" | jq -R 'split(".") | .[1] | @base64d | fromjson'

# Node.js
node -e 'console.log(Buffer.from(process.argv[1].split(".")[1], "base64url").toString())' "$TOKEN"

# Python
python3 -c 'import sys,base64; p=sys.argv[1].split(".")[1]; print(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)).decode())' "$TOKEN"

The Python line adds the missing = padding itself, which is the step that makes a plain urlsafe_b64decode() call fail. To turn exp into a date, date -u -d @1767229200 +%FT%TZ prints 2026-01-01T01:00:00Z with GNU date. A token pasted into a command also lands in your shell history, so read it from a file or clear the history line when you are done with a real one.

JWT decode functions in code: decode or verify

Most JWT libraries have both a decode and a verify path, and mixing them up is how unverified claims end up trusted. In the Node.js jsonwebtoken package, jwt.decode() returns the payload without checking the signature, and its README warns against using it for untrusted messages; jwt.verify() checks the signature and exp. Python's PyJWT works the other way round: jwt.decode() verifies by default and needs the key and an explicit list of algorithms. With PyJWT 2.10.1 and the example token:

>>> jwt.decode(TOKEN, options={"verify_signature": False})
{'sub': 'user_42', 'name': 'Ada', 'role': 'editor', 'iat': 1767225600, 'exp': 1767229200}

>>> jwt.decode(TOKEN, "wrong-secret", algorithms=["HS256"])
jwt.exceptions.InvalidSignatureError: Signature verification failed

>>> jwt.decode(TOKEN, "demo-secret-do-not-use", algorithms=["HS256"])
jwt.exceptions.ExpiredSignatureError: Signature has expired

The last call has the right secret and still fails, because the token's exp passed on 2026-01-01 at 01:00 UTC. Passing that list yourself, instead of reading it from the token header, is what protects you from the alg tricks mentioned above.

Reading the time claims

RFC 7519 defines exp (expiration), nbf (not before) and iat (issued at) as NumericDate values: seconds since 1970-01-01 00:00:00 UTC. In the example, iat 1767225600 is 2026-01-01 00:00:00 UTC and exp 1767229200 is one hour (3,600 seconds) later. The decoder turns each claim it finds into a Token Timing card in your local time zone, with a relative time such as "2 hours ago". A value in milliseconds instead of seconds is a common bug and shows up as a date tens of thousands of years in the future; the timestamp converter helps you check which unit you have.

The status line only compares exp with your computer's clock. A green "Token is VALID" message means the token hasn't expired yet, not that its signature is genuine, and a token without exp gets a notice that no expiration claim was found.

What to look for in your payload

When you decode JWT payloads, check the exp (expiration) and iat (issued at) claims; exp minus iat is the token's lifetime. I often find tokens that are valid for too long, which is a common security oversight. Check that iss (issuer) and aud (audience) name the services you expect. Then read the rest of the payload as if it were public, because to anyone holding the token, it is. RFC 7519 notes that a JWT "may contain privacy-sensitive information" and that leaving such information out of the token is "the simplest way of minimizing privacy issues". Email addresses, internal IDs and permissions are all readable without a key.

These are the registered claim names from RFC 7519, section 4.1, the ones you will see most often in a decoded payload:

ClaimNameWhat to check
issIssuerThe service you expect to have created the token
subSubjectThe user or client the token is about
audAudienceYour API, as a string or an array of strings
expExpiration timeSeconds since 1970; the token must not be used after it
nbfNot beforeSeconds since 1970; the token must not be used before it
iatIssued atSeconds since 1970; with exp it gives the lifetime
jtiJWT IDA unique ID for the token; a UUID is a common choice (see UUID v4 vs v7)

Anything else in the payload, such as name or role in the example, is a custom claim defined by whoever issued the token. The decoder's Copy Payload button gives you the formatted JSON, and the JSON formatter can collapse or search a long payload. If the JSON itself is new to you, what is JSON explains the syntax.

The hidden risks of online decoders

If a decoder sends your token to its server, whoever runs that server can replay the token until it expires. I've seen "free" tools that log tokens for "debugging" purposes. A local decoder removes that risk because the token never leaves the page, which is why I built ours that way.

Frequently asked questions

Does decoding a JWT verify the signature?

No. Decoding only shows you the data. Verification needs the secret (for HS256) or the issuer's public key (for RS256 and ES256), and it should happen on your server. Our tool is a decoder, not a validator: it shows you what's inside, but it doesn't check that the token hasn't been tampered with.

Can I decode a JWT without the secret key?

Yes, for signed tokens. The header and payload are only encoded, not encrypted, so anyone with the token can read them. That's why you shouldn't put secrets in a JWT payload. Encrypted tokens (JWE, five parts) are the exception.

Can I decode JWT tokens in the browser console?

Yes. JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))) works in any modern browser, because atob() accepts input without padding. It returns bytes as a Latin-1 string, though, so names with accents or emoji come out garbled. Our decoder converts the bytes as UTF-8 to avoid that.

Is it safe to paste an expired token?

It's much lower risk, because the server should reject it, but the payload can still contain personal data, so decode it locally anyway.


Share this article:
Mirsal Saidu

Mirsal Saidu

Digital & Performance Marketer