A JWT is easy to read and easy to misunderstand. It looks like an opaque credential, but a normal signed JWT is just three Base64url sections joined by dots:
header.payload.signature
The first two sections are JSON. Anyone with the token can decode them without a secret key. That is useful when you are debugging login, checking an exp timestamp, or confirming which aud a token was minted for. It is also why the payload is not a place for passwords, API keys, private profile data, or anything you would not show in a log.
The practical rule is simple: decode a JWT to inspect it; verify a JWT before trusting it.
Quick Answer
To decode a JWT:
- Remove an optional
Bearerprefix. - Split the token on dots.
- Base64url-decode the first section into the header JSON.
- Base64url-decode the second section into the payload JSON.
- Treat the third section as the signature bytes, not readable JSON.
- Read claims such as
sub,iss,aud,exp,nbf, andiat. - Verify the signature and required claims on the server before using the token for authorization.
No secret key is needed for step 1 through step 6. A key is needed only when you verify the signature.
What a JWT Looks Like
Here is a compact example token. It is useful for decoding practice, not a production credential:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMn0.X6sdNf_Fd-AFXKXrbTXHt3QuGJs6aTrwhYR6F4tZNIY
Split it at the dots:
| Section | Decodes to | What it is for |
|---|---|---|
eyJhbGci... |
Header JSON | Algorithm, type, key hint |
eyJzdWIi... |
Payload JSON | Claims about the subject |
X6sdNf... |
Signature bytes | Tamper detection |
Decoded header:
{
"alg": "HS256",
"typ": "JWT"
}
Decoded payload:
{
"sub": "1234567890",
"name": "Ada Lovelace",
"iat": 1516239022
}
The signature does not become a nice JSON object. It is cryptographic output over the encoded header and encoded payload. A decoder can show that a signature section exists, but only verification can prove whether it is valid.
JWT, JWS, and JWE in Plain English
These names are close enough to cause confusion:
| Term | What it means | Can you read the payload by decoding? |
|---|---|---|
| JWT | JSON Web Token, a claims format defined by RFC 7519 | Usually yes, when represented as a signed JWS |
| JWS | JSON Web Signature, integrity protection for a readable payload | Yes |
| JWE | JSON Web Encryption, encrypted payload for confidentiality | No, not without decryption |
Most web app "JWTs" are compact JWS tokens with three sections. They are signed, not encrypted. A compact JWE normally has five sections. If your token has five dot-separated parts, a simple JWT decoder will not show the claims because the payload is encrypted.
How to Decode a JWT in the Browser
JWT uses Base64url, not plain Base64. Base64url replaces + with -, replaces / with _, and often omits = padding. A robust decoder puts those details back before decoding.
This version also handles Unicode safely by decoding bytes through TextDecoder, which is better than using atob() output as text directly.
function base64UrlToJson(part) {
const base64 = part
.replace(/-/g, '+')
.replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
const binary = atob(padded);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
const json = new TextDecoder().decode(bytes);
return JSON.parse(json);
}
function decodeJwt(token) {
const trimmed = token.trim().replace(/^Bearer\s+/i, '');
const parts = trimmed.split('.');
if (parts.length < 2 || !parts[0] || !parts[1]) {
throw new Error('Not a JWT: expected header.payload.signature');
}
return {
header: base64UrlToJson(parts[0]),
payload: base64UrlToJson(parts[1]),
signature: parts[2] || ''
};
}
Use it for inspection:
const decoded = decodeJwt(token);
console.log(decoded.header.alg);
console.log(decoded.payload.sub);
console.log(decoded.payload.exp);
Do not use that decoded payload as proof of identity. The code above never checks whether the signature matches.
How to Decode a JWT in Node.js
Modern Node can decode Base64url directly with Buffer.
function decodePart(part) {
return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
}
function decodeJwt(token) {
const trimmed = token.trim().replace(/^Bearer\s+/i, '');
const [headerPart, payloadPart, signature = ''] = trimmed.split('.');
if (!headerPart || !payloadPart) {
throw new Error('Not a JWT: expected header.payload.signature');
}
return {
header: decodePart(headerPart),
payload: decodePart(payloadPart),
signature
};
}
This is still only decoding. In production, use a JOSE/JWT library to verify the token and enforce iss, aud, exp, nbf, and allowed algorithms.
How to Decode a JWT in Python
Python's base64.urlsafe_b64decode() handles the URL-safe alphabet, but you still need to restore padding.
import base64
import json
def decode_part(part):
padded = part + "=" * (-len(part) % 4)
decoded = base64.urlsafe_b64decode(padded)
return json.loads(decoded.decode("utf-8"))
def decode_jwt(token):
token = token.strip()
if token.lower().startswith("bearer "):
token = token[7:].strip()
parts = token.split(".")
if len(parts) < 2 or not parts[0] or not parts[1]:
raise ValueError("Not a JWT: expected header.payload.signature")
return {
"header": decode_part(parts[0]),
"payload": decode_part(parts[1]),
"signature": parts[2] if len(parts) > 2 else ""
}
If the token came from a real authorization header, avoid printing it in full. Log a short prefix or a hash, not the entire credential.
Reading the Header
The header is small, but it tells you how the token expects to be verified.
| Header field | Example | What to check |
|---|---|---|
alg |
RS256 |
The signing algorithm. Do not trust it blindly; allow-list expected algorithms in your verifier. |
typ |
JWT |
Usually informational. Some systems use it to distinguish token types. |
kid |
2026-07-key-1 |
Key ID. Use it only to look up a key from a trusted key set. |
cty |
JWT |
Content type, sometimes used for nested tokens. |
The two risky fields are alg and kid.
alg is dangerous when a server accepts whatever the token says. A forged token can claim alg: "none" or switch from an asymmetric algorithm to a symmetric one if the verifier is misconfigured. Good libraries let you pass an explicit allow-list such as ['RS256'].
kid is also attacker-controlled input. Treat it as a lookup hint inside a fixed JWKS, not as a file path, SQL value, or arbitrary URL.
Reading the Payload Claims
JWT payload claims are just JSON fields. Some are registered by the JWT standard; others are custom to your app.
| Claim | Meaning | Common debugging question |
|---|---|---|
iss |
Issuer | Did this token come from the identity provider I trust? |
sub |
Subject | Which user, service account, or principal is this token about? |
aud |
Audience | Was this token intended for my API? |
exp |
Expiration time | Is the token already expired? |
nbf |
Not before | Is the token being used too early? |
iat |
Issued at | When was it minted? |
jti |
JWT ID | Can this token be tracked or revoked? |
Time claims are Unix timestamps in seconds, not JavaScript milliseconds.
const expiresAt = new Date(payload.exp * 1000);
If you forget * 1000, your expiry date will look like it belongs near January 1970. If you multiply a value that was already milliseconds, it will jump far into the future. When debugging, inspect the raw number first.
Custom claims might look like this:
{
"tenant_id": "acme",
"role": "admin",
"scope": "read:invoices write:invoices"
}
Those fields are application policy, not JWT magic. Your server still needs to decide whether the subject may actually perform the requested action.
Decoding Is Not Verifying
This is the mistake that creates security bugs: a decoded payload is not a trusted payload.
Anyone can create this JSON:
{
"sub": "123",
"role": "admin",
"exp": 4102444800
}
Anyone can Base64url-encode it into a JWT-looking token. A decoder will show "role": "admin" just fine. Verification is what checks whether a trusted issuer actually signed that exact header and payload.
Use decoding for:
- Debugging what a token says.
- Checking whether a frontend received the expected claims.
- Inspecting expiration and audience values.
- Confirming whether a token is JWS-like or JWE-like.
Use verification for:
- Login sessions.
- API authorization.
- Role or permission decisions.
- Accepting tokens from another service.
- Anything that changes data.
A Safe Verification Checklist
Verification belongs on the server or in a trusted backend service. The exact API depends on your JWT library, but the checklist is consistent:
- Choose the expected algorithms in code. Do not accept every
algvalue from the token. - Select the verification key from trusted configuration or a trusted JWKS.
- Treat
kidas an untrusted key hint, not a free-form path or URL. - Verify the signature over the original encoded header and payload.
- Check
expandnbf, allowing only a small clock skew if needed. - Check
issagainst the expected issuer. - Check
audagainst your API or client ID. - Check required application claims such as tenant, role, scope, or session ID.
- Reject tokens that are missing required claims, not merely expired tokens.
Many libraries verify the signature and time claims by default but make issuer and audience checks opt-in. Read your library's options instead of assuming.
JWT Payloads Are Not Private
Base64url is encoding. It is not encryption. A signed JWT can be tamper-evident and still be readable by anyone who has it.
Do not put these in a normal JWT payload:
- Passwords
- API keys
- Refresh tokens
- Full credit-card numbers
- Medical or financial details
- Private profile fields that should not appear in logs
If claims need confidentiality, use JWE or keep the sensitive data server-side and send a short session identifier. In many web apps, the simpler session ID design is easier to protect than encrypted self-contained tokens.
For the broader encoding mistake, see Base64 Is Not Encryption.
How to Decode a JWT Locally
Paste a token into JWT Decoder when you need to inspect a token quickly. The tool runs in your browser and returns a JSON object with header and payload. It accepts a copied Bearer ... value and removes the prefix before decoding.
Because real JWTs are bearer credentials, use a local-first workflow:
- Prefer expired or test tokens when writing bug reports.
- Do not paste production tokens into random third-party tools.
- Redact tokens in screenshots and logs.
- If you only need to inspect one section, decode the Base64url section rather than sharing the whole token.
Local decoding reduces exposure, but it does not make the token harmless. Anyone who receives an unexpired bearer token may be able to use it.
Troubleshooting JWT Decode Errors
| Error or symptom | Likely cause | Fix |
|---|---|---|
Not a JWT |
Missing dots, empty header, or empty payload | Paste the compact token, not a JSON wrapper or quoted string |
Invalid JWT header |
First section is not Base64url JSON | Check for copy/paste truncation or a wrong token type |
Invalid JWT payload |
Second section is not Base64url JSON | Check for truncation, URL encoding, or encrypted JWE |
| Five dot-separated parts | Compact JWE, not normal signed JWT | Use a JWE library and decryption key |
| Expiry looks like 1970 | Treated seconds as milliseconds | Use new Date(exp * 1000) |
| Payload is readable but app rejects it | Signature, issuer, audience, or expiry failed | Verify with the same settings as the server |
The last row is common: readability does not mean validity. A token can decode perfectly and still be expired, forged, intended for another audience, or signed by the wrong issuer.
Frequently Asked Questions
How do I decode a JWT?
Remove an optional Bearer prefix, split the token on dots, Base64url-decode the header and payload, decode the bytes as UTF-8, then parse each part as JSON. The signature section is not human-readable JSON.
Do I need a secret key to decode a JWT?
No. A key is not needed to decode the header and payload because they are only Base64url-encoded. A secret or public key is needed to verify the signature and prove the token is authentic.
Is decoding a JWT the same as verifying it?
No. Decoding shows what the token claims. Verification checks the cryptographic signature and required claims such as issuer, audience, expiration, and not-before. Never authorize a request from decoded claims alone.
Can anyone read the data inside a JWT?
Yes, for normal signed JWTs. The payload is Base64url-encoded and signed, not encrypted. Anyone with the token can read the claims, so do not put secrets or private personal data in the payload.
What does the exp claim mean?
exp is the expiration time as a Unix timestamp in seconds. In JavaScript, convert it with new Date(exp * 1000). Verification libraries should reject expired tokens before your app trusts any claim.
What if my token has five sections instead of three?
A five-part compact token is usually JWE, which means the payload is encrypted. A normal JWT decoder can split it, but it cannot read the claims without the decryption key and a JWE-capable library.
What is the kid header used for?
kid is a key ID in the JWT header. It helps the verifier choose the right key from a trusted JWKS or key set. Treat it as untrusted input and never use it directly as a file path, SQL value, or URL.
Inspect Tokens Locally
- JWT Decoder - decode JWT header and payload locally in your browser.
- Base64 Encode & Decode - decode one Base64url section manually.
- Base64 Is Not Encryption - why readable token payloads are not private.
- Why Not to Paste Sensitive JSON Online - safer handling for tokens and payloads.
- Decode Base64 Strings and JWT Payloads - the practical Base64url guide.
- RFC 7519: JSON Web Token - the JWT standard summary.
Sources
- RFC 7519 - JSON Web Token.
- RFC 7515 - JSON Web Signature.
- RFC 7516 - JSON Web Encryption.
- RFC 7517 - JSON Web Key.
- RFC 8725 - JWT Best Current Practices.
- RFC 4648 - Base64 and Base64url encoding.
Last reviewed July 2026.