How JWT Uses Base64URL Encoding — Explained Simply

If you have ever inspected a JWT token during API debugging, you have probably seen something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIn0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

At first glance, it looks encrypted.

And that is exactly what most developers assume when they first encounter JWTs. The payload seems scrambled. The string looks like ciphertext. It must be protected by some kind of cryptography, right?

Here is the reality: JWT uses Base64URL encoding primarily for transport compatibility. Not security. Not encryption. Just a way to safely move JSON data through URLs, headers, and browser storage without characters breaking along the way.

This misunderstanding causes a surprising number of real-world bugs -- sensitive data exposed in payloads, incorrect token validation, broken JWT parsers, malformed authorization headers, and signature debugging nightmares that drag on for hours.

This article walks through what Base64URL actually is, why JWT depends on it, how the three-part JWT structure works, why payloads are readable by design, and the mistakes developers keep making in production.

Most importantly, it focuses on practical understanding rather than cryptography jargon. No hand-waving. Just what you actually need to know.


What Is a JWT?

JWT stands for JSON Web Token.

It is a compact token format used for authentication, authorization, OAuth flows, API security, and identity verification. JWT took off because it works well in microservices, stateless authentication, frontend-to-backend APIs, and mobile apps -- anywhere you need to pass verified claims between systems without a central session store.

A JWT is essentially three things rolled together: JSON data that carries claims about a user or request, encoded into a compact URL-safe string, and cryptographically signed so the receiver can verify it has not been tampered with.


JWT Structure: Three Parts Separated by Dots

Every JWT follows the same structure:

header.payload.signature

Three Base64URL-encoded sections, separated by dots. That is the entire format. No encryption layer. No obfuscation. Just encoded JSON with a signature tacked on the end.

Here is a real-looking example:

xxxxx.yyyyy.zzzzz

Each section serves a distinct purpose. Understanding what each part does is the key to debugging JWT issues effectively.

Part 1: The Header

The header tells the server which signing algorithm is in play and that the token is a JWT. A decoded header looks like this:

{
  "alg": "HS256",
  "typ": "JWT"
}

Nothing secret here. Just metadata. This JSON gets Base64URL-encoded and becomes the first segment:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Part 2: The Payload

The payload carries the actual claims -- information about the user or the request context. A typical payload:

{
  "userId": 123,
  "role": "admin",
  "exp": 1740000000
}

Common claims include user ID, email, expiration time, roles, and permissions. This JSON also gets Base64URL-encoded:

eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIiwiZXhwIjoxNzQwMDAwMDAwfQ

This is the part that surprises developers the most. The payload is completely readable by anyone who has the token. We will come back to why that matters.

Part 3: The Signature

The signature is where JWT security actually lives. The server creates it by combining the encoded header, the encoded payload, and a secret key through a cryptographic hash:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

If anyone modifies the payload -- say, changing "role": "user" to "role": "admin" -- the signature no longer matches, and the server rejects the token. This is the entire JWT security model in one sentence: the encoding is for transport, the signature is for trust.


What Is Base64URL?

Base64URL is a URL-safe variation of standard Base64 encoding.

Standard Base64 uses three characters that cause problems in web contexts:

+   /   =

The + and / characters have special meaning in URLs and query strings. The = padding character can confuse parsers and create issues in HTTP headers. When you stick a standard Base64 string into a URL, an authorization header, or a cookie, you are asking for trouble.

Base64URL fixes this with a simple character swap:

Standard Base64Base64URL
+-
/_
=removed (or made optional)

That is it. Two characters replaced, padding stripped. The result is a string that is safe for URLs, HTTP headers, cookies, and any other web transport mechanism.


Why JWT Uses Base64URL Instead of Standard Base64

Imagine a JWT embedded in a URL:

https://example.com/token=abc+/=

The + gets interpreted as a space in query strings. The / confuses path routing. The = gets mangled by URL parsers. The token breaks, and you spend an hour debugging an authentication failure that has nothing to do with your actual auth logic.

Base64URL avoids all of this by using only web-safe characters. JWT tokens pass through URLs, Authorization headers, cookie values, and redirect parameters without corruption.

This distinction -- Base64 vs Base64URL -- causes an enormous amount of debugging confusion. If you have ever tried to use a standard Base64 decoder on a JWT and gotten an "invalid character" error, you have hit this exact problem. For a deeper dive into the differences, Base64 vs URL Encoding walks through the technical details and edge cases.


JWT Is Not Encrypted

This is the single most common misconception about JWTs, and it is worth stating explicitly.

Developers see this:

eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIn0

And they think: "Nobody can read that."

But anyone can. Paste that string into a Base64URL decoder (or even just atob() in the browser console with a quick character fix), and you get:

{
  "userId": 123,
  "role": "admin"
}

JWT payloads are intentionally readable. Encoding is not encryption. This is the same confusion that surrounds Base64 in general -- Base64 is not encryption, it is just a way to represent binary data as text.

If you want a more detailed breakdown of the distinction between encoding and encryption in the JWT context, the companion piece JWT Is Not Encryption covers it thoroughly.


Real JWT Debugging Story

Here is a typical production debugging scenario that plays out in engineering Slack channels every week:

A developer on your team is integrating a third-party API. The frontend sends a JWT in the Authorization header. The backend rejects it with a 401. The developer copies the token from the Network tab, pastes it into a JWT decoder, and watches the payload appear in plain text.

The reaction is almost always the same: "Wait, anyone can read this?"

Then comes the follow-up realization: the token was not rejected because of encoding. It was rejected because the signature did not match -- maybe the secret key was different between environments, or the token had expired, or the algorithm in the header did not match what the server expected.

The payload being readable was never the security issue. The encoding was never meant to hide anything. The problem was entirely in the verification step.


What Actually Protects JWT Security

The signature. Not the encoding.

The signature guarantees two things: integrity (the payload has not been modified) and authenticity (the token was issued by a trusted source). It prevents attackers from safely modifying token contents.

Consider this attack scenario. An attacker intercepts a JWT with this payload:

{
  "role": "user"
}

They can decode it, change "user" to "admin", re-encode it, and send it back. The payload is visually modified. But the signature no longer matches -- it was computed from the original payload. The server detects the mismatch and rejects the token.

That is the entire security model. The attacker can read the payload. They cannot modify it without detection. Encoding enables transport. Signing enforces trust. Two completely separate concerns that developers frequently conflate.


Common Developer Mistakes With JWT Base64URL

Mistake 1: Putting Sensitive Data in the Payload

Since anyone with the token can decode the payload instantly, putting secrets there is a critical mistake.

Bad example -- never do this:

{
  "password": "super-secret-password"
}

Never place passwords, API keys, refresh secrets, personal financial data, or any other sensitive information inside JWT payloads. The payload travels through the network in the clear (from a readability standpoint). Anyone who inspects the token can see everything inside it.

Mistake 2: Using Standard Base64 Decoders on JWT Tokens

JWT uses Base64URL. Not standard Base64. If you feed a JWT segment into a standard Base64 decoder, you will get errors:

Invalid character
Incorrect padding
Malformed token

The - and _ characters are not valid in standard Base64, and the missing = padding breaks decoders that expect it. If you are debugging encoding errors like this, the guide on fixing invalid Base64 string errors covers the common failure patterns and how to resolve them.

Mistake 3: Forgetting About Missing Padding

JWT implementations typically strip the = padding characters to keep tokens compact. But some Base64 libraries (in Python, Java, and Go especially) refuse to decode without proper padding.

The fix is straightforward once you know to look for it: add back the missing = characters so the string length is a multiple of 4. But developers waste hours on this before they realize padding is the issue.

Mistake 4: Trusting Decoded Payloads Without Verification

This is the dangerous one:

jwt.decode(token)

Without signature verification. Anyone can forge payload contents and send them to your server. If your code trusts the decoded payload before verifying the signature, you have a security vulnerability.

The distinction between decoding and verifying is something developers keep getting wrong. The article JWT Decode vs Verify explains exactly where the line is and why it matters.


Decoding a JWT Payload in JavaScript

Here is how to decode a JWT payload in the browser or Node.js without any library:

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIn0.signature";

// Split on dots and grab the payload (middle segment)
const base64Url = token.split('.')[1];

// Convert Base64URL to standard Base64
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');

// Decode and parse as JSON
const payload = JSON.parse(atob(base64));

console.log(payload);
// { userId: 123, role: "admin" }

This works because atob() handles standard Base64. You just need the quick character swap from Base64URL to Base64 first, then the decoding proceeds normally.

But notice what this code does not do: verify the signature. Decoding and verification are entirely separate operations. Anyone can run the code above. Verification requires the secret key and a proper JWT library.


Proper JWT Verification in Node.js

Here is what verification looks like with the jsonwebtoken library:

const jwt = require('jsonwebtoken');

try {
  const verified = jwt.verify(token, SECRET_KEY);
  console.log(verified); // Payload is trustworthy
} catch (err) {
  console.log('Invalid token — signature mismatch, expired, or malformed');
}

Verification confirms three things at once: the token has not been tampered with since issuance (integrity), the token came from someone who knows the secret (authenticity), and the payload is exactly what the issuer intended.

If you just need to inspect a token during debugging, a decoder tool is the right choice. If your application code is making authorization decisions, you need full verification. No shortcuts.


Why JWT Became So Popular

Before JWT, most applications relied on server-side sessions. The server stored session data in memory or a database, and the client held a session ID cookie. That worked fine for monoliths but became a scaling headache with microservices and distributed systems.

JWT introduced a different model: put the relevant claims directly in the token, sign them so they are trustworthy, and let any service verify them independently without consulting a central session store.

The practical advantages are significant:

  • Portable -- the same token works across multiple services
  • Scalable -- no session store to hit on every request
  • Frontend-friendly -- works naturally with SPAs and mobile apps
  • Microservice-compatible -- each service can verify tokens independently

The tradeoffs are real too:

  • Payload visibility -- anyone with the token can read its contents
  • Difficult revocation -- you cannot easily invalidate a specific token before it expires
  • Token leakage risk -- a stolen token grants access until it expires

Whether JWT is the right choice depends on your architecture. But if you use it, understanding the encoding layer is non-negotiable.


JWT vs Session Cookies

A quick comparison for context:

Session Cookies

The server stores session state. The client only holds an opaque session ID.

Pros: easy logout, simple invalidation, nothing sensitive on the client.

Cons: requires server-side storage, more complex at scale, sticky sessions in load-balanced environments.

JWT

The client stores the token, which carries all relevant claims.

Pros: stateless, scalable, portable across services, no server-side storage overhead.

Cons: readable payloads, harder revocation, security misconceptions that lead to real vulnerabilities.

Neither approach is universally superior. The tradeoffs matter.


Why Base64URL Errors Happen So Often in JWT Debugging

JWT debugging frequently involves a cascade of encoding-related failures:

  • Malformed transport (a token that got corrupted during copy-paste or HTTP transmission)
  • URL corruption (characters getting encoded or decoded by intermediate proxies)
  • Incorrect decoding (using Base64 instead of Base64URL)
  • Whitespace issues (extra newlines or spaces from email clients or chat tools)
  • Expired tokens (the token is valid but old -- a different problem entirely)

Typical error messages developers see:

Invalid token
Malformed JWT
Invalid signature
Incorrect padding
token contains an invalid number of segments

Most of these are caused by Base64URL confusion, transport formatting problems, or copy-paste corruption -- not by actual security issues. The encoding layer is the source of most JWT debugging pain.


JWT, OAuth, and the Encoding Layer Cake

OAuth systems frequently combine JWT, Base64URL, URL encoding, cookies, and HTTP redirects -- all in the same flow. This creates an encoding layer cake where multiple transformations are stacked on top of each other.

A common scenario: a JWT (already Base64URL-encoded) gets placed in a URL query parameter, which then gets URL-encoded by the browser, which then gets decoded by the server's framework, which then passes the Base64URL string to the JWT library for verification. At any point in that chain, an encoding mismatch breaks everything.

Developers commonly confuse Base64URL, URL encoding, encryption, and signing as interchangeable concepts. They are not. Each layer serves a different purpose, and understanding where one ends and the next begins is the key to debugging these flows effectively.


FAQ

Is JWT encrypted?

Usually no. Most JWTs are Base64URL-encoded and cryptographically signed, but not encrypted. The payload is readable by anyone who has the token. There is a JWE (JSON Web Encryption) standard that does encrypt tokens, but it is far less common than signed-but-unencrypted JWS tokens.

Why can anyone read JWT payloads?

Because Base64URL encoding is fully reversible -- it is an encoding, not encryption. JWT payloads are intentionally readable so that services can inspect claims without needing to decrypt anything. If you need confidentiality, you need a different mechanism (like JWE or HTTPS for transport).

What protects JWT security?

The cryptographic signature. It prevents unauthorized token modification by making any change to the header or payload detectable through signature mismatch. The signature is computed from the token contents and a secret key, so anyone who does not know the secret cannot produce a valid signature for a modified token.

What is Base64URL?

A URL-safe variation of Base64 encoding. It replaces + with -, / with _, and removes = padding. JWT uses it to avoid problematic characters that would break URLs, query parameters, and HTTP headers.

Why does JWT remove the = padding?

To keep tokens shorter and cleaner for transport. The = padding adds visual noise and can cause parsing issues in some contexts. Most JWT libraries handle the missing padding automatically during decoding.

Is decoding a JWT the same as verifying it?

No. Decoding only reads the payload data -- anyone can do it with a few lines of JavaScript. Verification confirms integrity, authenticity, and that the signature was produced by a trusted source. Decoding is for inspection. Verification is for trust. Conflating the two is a security risk.

What happens if I put sensitive data in a JWT payload?

Anyone who obtains the token can decode and read that data. Do not put passwords, API keys, refresh secrets, financial information, or any other sensitive data in JWT payloads. The payload is visible by design.


Get Unstuck Faster

JWT debugging goes in circles when you cannot quickly separate encoding problems from verification problems from expiration problems. A dedicated JWT decoder gives you an instant read on what is inside any token -- no code, no console, no character-swapping gymnastics.

The JWT Decoder handles Base64URL decoding properly, shows you the header and payload at a glance, and lets you verify signatures with your own secret so you can tell whether a rejection is a verification failure or something else entirely.

If you are neck-deep in an OAuth integration or chasing a 401 that will not go away, give it a shot. Sometimes seeing the decoded payload in front of you is all it takes to realize the problem was never what you thought it was.