The first time I saw a JWT token, I thought it was encrypted. It looked like complete nonsense — a wall of random characters with two dots in it.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoidXNlciIsImlhdCI6MTc0MTIwMDAwMCwiZXhwIjoxNzQxMjAzNjAwfQ.8vQT2mK9pL3nX1wR4hY7cZ6bN5jM0sA2eD9fU8gV
Looks secure. Looks scrambled. Looks like nobody could read it.
Turns out anyone can read it. In about three seconds.
Base64 Encoding Is Not Encryption
This is the big one. The thing that trips up a lot of beginners, including me.
Base64 is just a way to convert data into a safe format for sending over the internet. It takes any data — text, JSON, binary — and turns it into a string made of only letters, numbers, and a few symbols.
It's not scrambling. It's not hiding anything. It's just converting.
Anyone in the world can decode a Base64 string back to its original content instantly. No key needed. No password. Just paste it into a decoder and read it.
Not encrypted. Just formatted.
So Why Does the Token Look Like Gibberish?
Because JSON with special characters, spaces, and curly braces doesn't travel well across HTTP headers and URLs. Some systems choke on certain characters. Some strip spaces. Some break on curly braces.
Base64 solves this by converting everything into a small, safe set of characters that work everywhere. The token looks scrambled but it's just regular JSON in a travel-friendly format.
Think of it like packing a suitcase. Your clothes look like a squished mess inside, but unpack them and they're just your clothes. Nothing changed. Just compressed for the journey.
The Three Parts of a JWT Token
See those two dots in the token? They split it into three separate parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header
.
eyJ1c2VySWQiOjQyLCJyb2xlIjoidXNlciJ9 ← Payload
.
8vQT2mK9pL3nX1wR4hY7cZ6bN5jM0sA2eD9 ← Signature
Each part is Base64URL encoded separately. Let's see what's actually inside each one.
Part 1: The Header
Decode that first chunk and you get plain JSON:
{
"alg": "HS256",
"typ": "JWT"
}
Just says what algorithm was used to sign it and that it's a JWT. Nothing secret. Nothing sensitive. Readable by anyone.
Part 2: The Payload
This is where your actual data lives. Decode the middle chunk:
{
"userId": 42,
"role": "user",
"iat": 1741200000,
"exp": 1741203600
}
Completely readable. User ID, role, when it was created, when it expires. All of it is just sitting there in plain JSON.
This is why you should **never put sensitive data in a JWT payload**. No passwords. No credit card numbers. No private information. Anyone who intercepts the token can read it without any special tools.
Part 3: The Signature
This is the only part that actually provides security. And this part can't be decoded the same way.
The signature is created by taking the header and payload, combining them, and running them through a hashing algorithm using your secret key. The result is a unique fingerprint for that specific token with that specific content.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your_secret_key
)
If anyone changes even one character in the header or payload — say, changing "role": "user" to "role": "admin" — the signature won't match anymore. Your server rejects the token.
That's what makes JWT secure. Not the encoding. The signature.
Let Me Prove the Payload Is Readable
Here's a quick way to decode any JWT payload yourself in Node.js:
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoidXNlciJ9.signature';
// Split by dots, grab the payload (middle part)
const payloadBase64 = token.split('.')[1];
// Decode it - no key, no password, just decode
const payload = JSON.parse(
Buffer.from(payloadBase64, 'base64').toString('utf8')
);
console.log(payload);
// { userId: 42, role: 'user' }
Four lines of code. No secret key. Anyone can do this.
If that surprises you, good. Now you understand why what you put in the payload matters.
Base64 vs Base64URL — What's the Difference?
JWT uses a slightly modified version called Base64URL. Regular Base64 uses + and / characters, which cause problems in URLs.
Base64URL swaps those out:
+becomes-/becomes_- Trailing
=padding is removed
That's why JWT tokens look slightly different from regular Base64. Same concept, just URL-safe characters. You can drop a JWT straight into a URL parameter without it breaking.
So Where Does the Security Actually Come From?
Just to be crystal clear on this:
- Base64 encoding → just formatting, zero security
- The payload → readable by anyone, don't put secrets here
- The signature → this is the actual security
- Your secret key → this is what makes the signature trustworthy
The whole security of a JWT rests on one thing: your secret key being genuinely secret and genuinely random.
If your key is weak, someone can brute-force it. If your key leaks, they can forge tokens. The Base64 encoding does absolutely nothing to protect you from either of those problems.
That's why I always generate my JWT secret keys properly — using jwtsecretkeygenerator.com rather than typing something myself. The encoding part is handled automatically by the library. The key is the only part you actually control, so that's where the effort should go.
What About JWE? (The Actually Encrypted Version)
If you genuinely need to hide the payload contents — not just protect their integrity — there's a standard called JWE (JSON Web Encryption).
JWE actually encrypts the payload. Someone who intercepts the token can't read what's inside without the decryption key.
Most apps don't need this. Keeping sensitive data out of the payload entirely is simpler and usually fine. But if you're storing anything in the token that shouldn't be visible even in transit, JWE is worth looking into.
The Practical Takeaway
Next time you look at a JWT token, you know what's happening. The dots split it into three parts. Two of them are just JSON in Base64 format — readable by anyone. The third is a signature that proves nothing was changed.
The token isn't hidden. It's just structured.
Don't put sensitive data in the payload. Do use a strong secret key for the signature. Those two rules cover most of what you need to know about JWT security in practice.
💡 Quick Test: Copy any JWT token and paste it into jwt.io. You'll see the header and payload decoded instantly, no key required. That's Base64 encoding in action — and a good reminder of why your payload shouldn't contain anything private.