How to Decode JWT, Base64url, and URL-Encoded Data Safely
Decode JWT claims, Base64 and Base64url bytes, and percent-encoded URL values locally; handle Unicode correctly and understand why encoding is neither encryption nor JWT verification.
A JWT is easy to read and easy to misread. It appears to be an opaque credential, but a standard signed JWT is simply three sections divided by dots, Base64url encoded:
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.
Base64 Is Encoding, Not Encryption
Base64 is an encoding to convert bytes to printable text. Normally three input bytes become four characters, so the output is approximately one-third larger before compression. No password, no secret key, no cryptographic proof.
btoa('hello'); // aGVsbG8=
atob('aGVsbG8='); // hello
The browser functions operate on byte-like “binary strings,” not arbitrary Unicode text. Use UTF-8 explicitly:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
Base64 is good for transmitting binary data over JSON, MIME, a header, or another text channel. It is not suitable for hiding a frontend API key, storing a password, securing Basic Auth on plain HTTP, or keeping URL state private. Use TLS for transport, authenticated encryption for confidentiality, password hashing for passwords, signatures or MACs for integrity. Base64 may wrap the output of those operations but it does not provide their security.
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 doesn’t convert into a nice JSON object. It is cryptographic output over encoded header and encoded payload . A decoder may demonstrate that there is a signature section, but only verification can demonstrate that 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 JWTs on web apps are short JWS tokens with three parts . They are signed not encrypted A typical JWE is compact and has five parts. A simple JWT decoder won’t show the claims with 5 dot separated parts, 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 was a real authorization header, do not print in full Log a short prefix or a hash, not the whole 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
The JWT payload claims are just JSON fields. Some are registered by the JWT standard, some are specific 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 not JWT magic, they are application policy. Your server still needs to determine if the subject is actually allowed to do the action requested.
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 should be done on the server or a trusted back-end service. The API itself depends on your JWT library, but the checklist is the same:
- 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 an encoding. That’s not encrypting. A signed JWT can be tamper evident , but still readable to anyone that 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
Use JWE if you want the claims to be secret . Or store the sensitive data on a server and send a short session identifier . For many web apps, simpler session ID designs are easier to secure than encrypted self-contained tokens.
The Base64 section at the start of this article explains the broader encoding mistake and the correct security boundary.
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 the exposure, but it does not make the token harmless. Anyone who has an unexpired bearer token may be able to use it.
URL Encoding Is a Separate Layer
Percent-encoding represents UTF-8 bytes as %XX sequences so data cannot be mistaken for URL structure. It is not Base64 and provides no secrecy.
Use the API that matches the boundary:
// One path segment or query value: escape /, ?, &, =, and #.
encodeURIComponent('reports/2026?team=R&D');
// reports%2F2026%3Fteam%3DR%26D
// A complete URL: preserve structural separators.
encodeURI('https://example.com/search?q=hello world');
// https://example.com/search?q=hello%20world
For query strings, prefer URL and URLSearchParams over concatenation:
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'hello world');
url.searchParams.append('tag', 'R&D');
URLSearchParams uses form-style encoding, where a space serializes as +. decodeURIComponent() does not convert + to a space; URLSearchParams does. This distinction matters when decoding raw query values.
If %20 becomes %2520, the value was probably encoded twice: %25 is the encoded percent sign. Fix the pipeline so each component is encoded exactly once at its boundary. Do not repeatedly decode untrusted input “until it stops changing”; extra decoding can turn data into delimiters such as /, &, or .. after a security check has already run.
JWT compact tokens use the URL-safe Base64 alphabet so that their segments generally survive as path or query data. A copied token could still be percent-encoded by an enclosing URL, form or JSON string. Then delete just the layer you know the transport added, and split and decode the token.
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 |
Commonality of last row: readability is not validity. A token can be expired, forged, for another audience, or signed by the wrong issuer, and will still decode perfectly.
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?
Nope. The header and the payload are just Base64url-encoded, and don’t need a key to decode them. To check the signature and show the authenticity of the token, a secret or public key is needed.
Is decoding a JWT the same as verifying it?
No. Decoding reveals what the token claims. Verification checks the cryptographic signature and required claims, such as issuer, audience, expiration and not-before. Never permit based upon decoded claims alone.
Can anyone read the data inside a JWT?
Yes, for regular signed JWTs. The payload is not encrypted, it is Base64url-encoded and signed. Anyone with the token can read the claims, so don’t 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 compact token usually has five parts and is a JWE, i.e. the payload is encrypted. A normal JWT decoder can parse it but it can not 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.
Is Base64 encrypted?
Nope. That means that anyone with the encoded value can decode it without a key. Base64 is good for encoding bytes as text . Confidentiality = encryption + proper key management .
What is the difference between Base64 and Base64url?
Base64url replaces + and / with - and _ and commonly omits padding. It is friendlier to URLs and filenames but has exactly the same security properties as standard Base64.
Should I use encodeURI or encodeURIComponent?
Use encodeURIComponent() for one path segment, query key, or query value. Use encodeURI() only when you already have a complete URL whose structural characters must remain. Prefer URL and URLSearchParams when building query strings.
Inspect Tokens Locally
- JWT Decoder - decode JWT header and payload locally in your browser.
- Base64 Encode & Decode - decode one Base64url section manually.
- Fix JSON Online - safer local handling for sensitive tokens and payloads.
- URL Encode & Decode - inspect percent-encoded components locally.
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.
- RFC 3986 - URI syntax and percent-encoding.
- MDN: URLSearchParams - query-string encoding and parsing.
Last reviewed August 2026.