Yes, you can decode a JWT without the secret. A signed JWT's header and payload are only base64url-encoded JSON, so splitting the token at the dots and decoding the first two parts shows every claim. The secret or public key is needed only to verify the signature, which is the step that tells you whether the token is genuine and unchanged.
That difference matters in both directions. Developers can inspect tokens freely while debugging, and anyone who holds your token can read it too. Decoding is never a substitute for verification on the server.
What is inside a JWT
RFC 7519 defines a JWT as a sequence of URL-safe parts separated by periods, each part base64url-encoded. A signed token (a JWS, defined in RFC 7515) has three parts:
| Part | Contains | Needs a key to read? |
|---|---|---|
| Header | JSON such as {"alg":"HS256","typ":"JWT"} |
No |
| Payload | JSON claims such as sub, exp, iat |
No |
| Signature | Bytes computed over header.payload with the key |
It is just bytes; the key is needed to check it |
Base64url (RFC 4648, section 5) is ordinary Base64 with - and _ in place of + and /, and JWTs drop the = padding. To decode a part, you swap those two characters back, add padding if your decoder needs it, decode the Base64 and parse the JSON.
Here is a sample token used throughout this article:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9.aWbbNTzOs7uEA6DpB2uelr-7-QYf31QJPnzbaTj3aSA
It decodes to the header {"alg":"HS256","typ":"JWT"} and the payload {"sub":"1234567890","name":"Jane Doe","iat":1767225600,"exp":1767229200}. The iat and exp values are NumericDate, seconds since 1970-01-01T00:00:00Z, so this token was issued at 2026-01-01 00:00 UTC and expired one hour later.
Decode a JWT in code
JavaScript (browser)
function decodeJwtPart(part) {
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const bytes = Uint8Array.from(atob(padded), c => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes)); // handles UTF-8 such as "Zoë"
}
const [header, payload] = token.split(".");
console.log(decodeJwtPart(header), decodeJwtPart(payload));
atob on its own returns one character per byte, which garbles non-ASCII text; TextDecoder turns the bytes back into UTF-8 correctly.
Node.js
const [h, p] = token.split(".").slice(0, 2)
.map(s => JSON.parse(Buffer.from(s, "base64url").toString("utf8")));
Python
import base64, json
def b64url_decode(part):
return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
header, payload, _sig = token.split(".")
print(json.loads(b64url_decode(header)))
print(json.loads(b64url_decode(payload)))
Command line (bash)
p=$(printf '%s' "$JWT" | cut -d. -f2 | tr '_-' '/+')
while [ $(( ${#p} % 4 )) -ne 0 ]; do p="$p="; done
printf '%s' "$p" | base64 -d
Each of these snippets printed the payload above when run against the sample token. If you would rather not write code, paste the token into the JWT decoder; it runs in your browser and converts the dates for you.
Reading the dates in the payload
The time claims are plain numbers, so a decoded payload is easier to check once they are converted. RFC 7519 defines NumericDate as seconds since 1970-01-01T00:00:00Z UTC, ignoring leap seconds, while JavaScript dates count milliseconds:
const exp = 1767229200; // from the payload
new Date(exp * 1000).toISOString(); // "2026-01-01T01:00:00.000Z"
Date.now() / 1000 > exp; // true once the token has expired
from datetime import datetime, timezone
datetime.fromtimestamp(1767229200, tz=timezone.utc).isoformat()
# '2026-01-01T01:00:00+00:00'
Three claims matter most when a token is rejected:
expin the past: the token has expired and must be refreshed.nbfin the future: the token is not valid yet, often a clock difference between servers.iatfar in the past: an old token is being reused, for example from a cached session.
Why decoding is not verification
Anyone can create a token with any payload and encode it correctly. What they cannot do without the key is produce a valid signature. Verification therefore has two halves:
- Check the signature with the expected algorithm and key. For HS256 that means recomputing an HMAC-SHA256 over
header.payloadwith the shared secret and comparing it in constant time. For RS256 or ES256 it means checking the signature with the issuer's public key. - Validate the claims. RFC 7519 says the current time must be before
exp. RFC 8725, the JWT best current practices, adds that applications must validate the audience and check that the issuer and subject are the ones they expect.
RFC 8725 also describes the classic mistakes that come from trusting the header too much:
alg: none. An attacker changes the algorithm tonone, and a careless library accepts the token with no signature check. RFC 8725 saysnoneshould only be used when the JWT is protected by other means.- Algorithm confusion. An
RS256token is changed toHS256and signed with the public key as if it were an HMAC secret. Libraries must let the caller fix the allowed algorithms instead of reading them from the token.
A good rule: the server decides which algorithm and key to use, and the token's header only has to agree.
Decoding vs verifying at a glance
| Decode | Verify | |
|---|---|---|
| Needs secret or public key | No | Yes |
| Shows claims | Yes | Yes |
| Proves who issued the token | No | Yes |
| Detects tampering | No | Yes |
Checks exp, aud, iss |
Only if you read them yourself | Yes, as part of validation |
| Safe place to do it | Anywhere, including the browser | On the server that trusts the token |
What this means for token design
- Never store secrets in claims. Passwords, API keys and personal data you would not show the user should stay out of the payload.
- Keep tokens short-lived. A readable token that leaks is less harmful if
expis close. - Use encryption when the payload must stay private. A JWE (five parts) encrypts the claims; only the protected header is readable without the key.
- Treat client-side decoding as display only. A front end may read
nameorexpto update the interface, but access decisions belong to the server.
Related encodings
JWT parts are one of many places where Base64 appears. Base64 vs hex encoding explains why tokens use Base64 rather than hex, and why Base64 makes data about 33% larger shows what that costs in size. HMAC signatures are built on hash functions; the MD5 vs SHA-256 guide covers why SHA-256 replaced older hashes. For quick one-off decoding of a single part, the Base64 operations on the universal encoder decoder also work once you swap - and _ back to + and / and add = padding.
Frequently asked questions
Can anyone read the contents of my JWT?
Yes, for a normal signed JWT. The header and payload are base64url-encoded JSON, not encrypted, so anyone who has the token can read them. Do not put passwords, API keys or other secrets in JWT claims.
What is the difference between decoding and verifying a JWT?
Decoding turns the base64url text back into JSON and needs no key. Verifying recomputes or checks the signature with the secret (HMAC) or public key (RSA, ECDSA) and then validates claims such as exp, iss and aud. Only verification tells you the token is genuine.
Can I decode an encrypted JWT (JWE) without the key?
No. A JWE has five parts and its payload is ciphertext. You can read its protected header, but the claims need the decryption key.
Read the header and payload of a JSON Web Token. Shows exp and iat as dates.
Open the JWT Decoder