A JWT (JSON Web Token) is three Base64url-encoded sections joined by dots. Decoding one to read its claims takes a single function call and no secret key — which is exactly why a JWT payload is not a safe place for sensitive data. This guide explains what a JWT is, how to decode each part, and the critical difference between decoding and verifying a token.
What Is a JWT?
A JWT is a compact, URL-safe token used to carry claims — typically a user's identity and permissions — between parties. It was standardised as RFC 7519. A token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe two dots split it into three parts: header, payload, and signature.
The Three Parts
- Header — a small JSON object describing the signing algorithm, e.g.
{"alg":"HS256","typ":"JWT"}. - Payload — a JSON object of claims such as
sub(subject),exp(expiry), and any custom fields. - Signature — a cryptographic signature over the header and payload, used to detect tampering. It is not human-readable and is not JSON.
The header and payload are Base64url-encoded JSON. The signature is produced by JWS (RFC 7515).
How to Decode a JWT in JavaScript
Split on the dots and Base64url-decode the first two parts. Note that JWT uses Base64url (with - and _ instead of + and /, and no padding), so convert before decoding:
function decodeJwt(token) {
const [headerB64, payloadB64] = token.split('.');
const decode = (b64url) => {
// Base64url → Base64, then decode and parse
const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
const json = atob(b64);
return JSON.parse(json);
};
return { header: decode(headerB64), payload: decode(payloadB64) };
}
decodeJwt(token);
// {
// header: { alg: "HS256", typ: "JWT" },
// payload: { sub: "1234", name: "Ada" }
// } For full Unicode safety, decode the Base64 to bytes and run them through TextDecoder rather than atob alone.
How to Decode a JWT in Python
import base64, json
def decode_jwt(token):
header_b64, payload_b64, _sig = token.split('.')
def decode(part):
# Add back padding Base64url strips
padded = part + '=' * (-len(part) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
return decode(header_b64), decode(payload_b64)Decoding Is Not Verifying
This is the most important point about JWTs. Anyone can decode a token and read its payload — no key required. Decoding tells you what the token claims; it does not tell you whether those claims are trustworthy.
To trust a token you must verify the signature against the server's secret or public key, using a vetted library — never roll your own:
import jwt from 'jsonwebtoken';
// Verifies the signature AND checks expiry; throws if invalid
const claims = jwt.verify(token, process.env.JWT_SECRET);
// jwt.decode(token) — does NOT verify; use only for inspectionVerification on the server is what makes a JWT a credential. Treating a decoded payload as trusted — without checking the signature — is a classic authentication bug.
A JWT Payload Is Not Encrypted
Because the payload is just Base64url-encoded JSON, it offers zero confidentiality. This is the same misconception covered in Base64 Is Not Encryption: encoding is reversible by anyone.
Never put passwords, full credit-card numbers, or sensitive personal data in a JWT payload. If you genuinely need an encrypted token, use JWE (JSON Web Encryption, RFC 7516) — a separate, less common standard. And avoid pasting real tokens into untrusted online decoders; see why you shouldn't paste sensitive JSON into online tools.
Validation Order: Signature First, Then Claims
When you verify a token in production, the order matters. A safe checklist:
- Algorithm allow-list — reject anything not in your expected set (e.g.
['RS256']). Never acceptnone. - Signature — verify against the right key (look up by
kidin your JWKS). exp— token must not be expired.nbf(not-before) — token must not be used too early.iat— sanity-check the issued-at isn't absurdly in the future (clock skew aside).iss— issuer must match the trusted authority you expect.aud— audience must include your service.
Most JWT libraries do steps 1–4 by default and require you to opt in for 6–7. Until iss and aud are checked, a token valid for some other service in your ecosystem can be replayed against yours.
Common Standard Claims
| Claim | Meaning |
|---|---|
iss | Issuer — who created the token |
sub | Subject — who the token is about (often a user ID) |
aud | Audience — who the token is intended for |
exp | Expiry — Unix timestamp after which the token is invalid |
iat | Issued-at — Unix timestamp when the token was created |
nbf | Not-before — token isn't valid until this time |
The kid Header and the alg: none Pitfall
Two header fields show up so often in real JWTs that they're worth knowing on sight:
kid(key ID) — a hint that tells the verifier which key from a key set (JWKS) signed this token. It belongs in the header so the verifier can pick the right key before checking the signature. Treatkidas untrusted input: look it up in a fixed allow-list (or a fetched JWKS), never use it as a file path, a SQL parameter, or a URL.alg: "none"— a legal but historically dangerous algorithm that means "no signature." Decoders happily decode it, and a permissive verifier may accept it, turning any forged payload into a "valid" token. Rejectalg: noneexplicitly, and configure your library with the exact algorithm(s) you expect (e.g.['RS256']) instead of trusting whatever the token's own header claims.
Decode a JWT in Your Browser
To inspect a token's claims quickly, paste either Base64url section into fixjson's Base64 decoder — it decodes to the underlying JSON locally, with nothing sent to a server. For non-sensitive tokens you can also use a dedicated debugger, but for anything containing real user data, a local-first tool is the safe choice.
Frequently Asked Questions
How do I decode a JWT?
Split the token on its dots and Base64url-decode the first two parts (header and payload) into JSON. The third part is the signature and isn't human-readable. No key is needed to decode.
Is decoding a JWT the same as verifying it?
No. Decoding just reads the claims; anyone can do it. Verifying checks the signature against a secret or public key to confirm the token is authentic and untampered. Always verify on the server before trusting a token.
Can anyone read the data inside a JWT?
Yes. The payload is Base64url-encoded, not encrypted — it's plaintext to anyone who has the token. Never store secrets in a JWT payload. See Base64 Is Not Encryption.
What's the difference between JWT, JWS, and JWE?
A signed JWT uses JWS (RFC 7515) — readable payload, tamper-evident. JWE (RFC 7516) actually encrypts the payload for confidentiality. Plain JWTs you see day-to-day are JWS.
What is the kid claim used for?
kid is a header field (not a payload claim) that identifies which key from a JWKS signed the token, so the verifier can pick the right one. Look it up against a fixed key set you control — never trust it as a file path or URL.
Why is alg: "none" dangerous?
It means "no signature." A permissive verifier that honours the token's own algorithm field will accept any forged payload. Always configure your library with the exact algorithm you expect (e.g. ['RS256']) and explicitly reject none.
Inspect Tokens Locally
- Base64 Encode & Decode — decode JWT header/payload sections in your browser
- Base64 Is Not Encryption — why a JWT payload is readable by anyone
- Why Not to Paste Sensitive JSON Online — handling real tokens safely
- RFC 7519: JSON Web Token — the standard itself