← All articles

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:

  1. Remove an optional Bearer prefix.
  2. Split the token on dots.
  3. Base64url-decode the first section into the header JSON.
  4. Base64url-decode the second section into the payload JSON.
  5. Treat the third section as the signature bytes, not readable JSON.
  6. Read claims such as sub, iss, aud, exp, nbf, and iat.
  7. 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:

  1. Choose the expected algorithms in code. Do not accept every alg value from the token.
  2. Select the verification key from trusted configuration or a trusted JWKS.
  3. Treat kid as an untrusted key hint, not a free-form path or URL.
  4. Verify the signature over the original encoded header and payload.
  5. Check exp and nbf, allowing only a small clock skew if needed.
  6. Check iss against the expected issuer.
  7. Check aud against your API or client ID.
  8. Check required application claims such as tenant, role, scope, or session ID.
  9. 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

Sources

Last reviewed August 2026.