Base64 and Base64URL Encoding in JWTs Explained
If you have ever pasted a JWT into a debugger and wondered why it looks like random characters instead of JSON, you have encountered Base64URL encoding. JWTs are not encrypted — their header and payload are simply encoded so binary-safe JSON can travel in URLs, headers, and cookies without corruption.
This guide explains Base64 and Base64URL encoding, why JWTs use the URL-safe variant, and how to encode and decode token parts correctly.
Why JWTs Need Encoding
JWT headers and payloads are JSON objects. JSON is text, but JWTs must be safe to embed in:
- URL query parameters
- HTTP
Authorizationheaders - Cookies without additional escaping
- WebSocket messages
Standard JSON contains characters like quotes, spaces, and braces that require escaping in many transport contexts. Base64 encoding converts arbitrary bytes into a limited alphabet of 64 URL-safe characters.
Base64 vs Base64URL
Both encodings represent binary data as ASCII text, but they differ in character selection:
| Feature | Base64 | Base64URL |
|---|---|---|
| Character 62 | `+` | `-` |
| Character 63 | `/` | `_` |
| Padding | `=` appended | `=` omitted |
| Safe in URLs | No (`+` becomes space) | Yes |
| Used in JWTs | No | Yes (RFC 7519) |
The Base64URL glossary entry covers the specification details. The practical takeaway: never use standard Base64 functions to decode JWTs without accounting for these differences.
JWT Encoding in Practice
A JWT header like {"alg":"HS256","typ":"JWT"} encodes to:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9A payload like {"sub":"user-123","exp":1735689600} encodes to:
eyJzdWIiOiJ1c2VyLTEyMyIsImV4cCI6MTczNTY4OTYwMH0The signature (third part) is also Base64URL-encoded, but it represents raw HMAC or RSA output bytes, not JSON.
Encoding and Decoding
JavaScript
// Encode JSON to Base64URL
function encodeBase64Url(obj) {
const json = JSON.stringify(obj);
const base64 = btoa(json);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Decode Base64URL to JSON
function decodeBase64Url(str) {
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const padding = base64.length % 4;
if (padding) base64 += '='.repeat(4 - padding);
return JSON.parse(atob(base64));
}
const header = decodeBase64Url('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
// { alg: 'HS256', typ: 'JWT' }Python
import base64, json
def encode_base64url(obj):
data = json.dumps(obj, separators=(',', ':')).encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def decode_base64url(s):
padding = 4 - len(s) % 4
s += '=' * padding
return json.loads(base64.urlsafe_b64decode(s))Online Tools
Use the Base64 Encoder/Decoder for quick conversions during development. For full JWT construction, the JWT Encoder handles header, payload, and signature encoding together.
The Three JWT Parts
BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)| Part | Content Before Encoding | After Encoding |
|---|---|---|
| Header | JSON object | ASCII string |
| Payload | JSON object | ASCII string |
| Signature | Raw bytes (HMAC/RSA output) | ASCII string |
Only the header and payload are human-readable after decoding. The signature is binary data that happens to be Base64URL-encoded for transport.
Common Encoding Mistakes
Using Standard Base64 on JWTs
// WRONG — standard Base64, not Base64URL
const payload = JSON.parse(Buffer.from(part, 'base64').toString());
// CORRECT — Base64URL with padding restoration
let b64 = part.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
const payload = JSON.parse(Buffer.from(b64, 'base64').toString());Forgetting Padding
Base64URL omits trailing = padding. Decoders must add it back before decoding. Most libraries handle this automatically, but manual decoders often fail without padding restoration.
Pretty-Printing JSON Before Encoding
JWT libraries serialize JSON without whitespace:
// Correct — compact JSON
{"alg":"HS256","typ":"JWT"}
// Wrong — pretty-printed (different encoding output)
{
"alg": "HS256",
"typ": "JWT"
}Signatures are computed over the exact encoded bytes. Reformatting JSON changes the encoding and breaks signature verification.
Confusing Encoding with Encryption
Base64URL is a reversible encoding, not encryption. Anyone can decode a JWT payload and read its contents. Sensitive data belongs server-side, not in JWT claims.
Building a JWT Manually
Understanding encoding helps you debug token issues:
1. Create the header JSON: {"alg":"HS256","typ":"JWT"}
2. Base64URL-encode the header
3. Create the payload JSON: {"sub":"user-123","exp":1735689600}
4. Base64URL-encode the payload
5. Compute HMAC-SHA256 over encodedHeader.encodedPayload
6. Base64URL-encode the signature bytes
7. Join with dots: header.payload.signature
The JWT Encoder automates all seven steps and shows intermediate encoding at each stage.
Base64 in Other Contexts
Base64URL appears beyond JWTs:
| Context | Encoding | Notes |
|---|---|---|
| JWT header/payload | Base64URL | RFC 7519 |
| JWT signature | Base64URL | Raw HMAC/RSA bytes |
| Data URLs | Standard Base64 | `data:image/png;base64,...` |
| API key transport | Standard Base64 or hex | Context-dependent |
| JWK modulus/exponent | Base64URL | JSON Web Keys (RFC 7517) |
Use the Base64 tool when working with non-JWT Base64 data to avoid mixing encodings.
Security Implications
Because encoding is not encryption:
- Never store passwords, API keys, or PII in JWT payloads
- Assume any client can read every claim in the token
- Rely on HTTPS for transport confidentiality
- Rely on HMAC/RSA signatures for integrity and authenticity
Decoding a JWT reveals its contents. Verifying the signature proves they have not been tampered with. Both concepts are essential — see how to decode a JWT without the secret for the full picture.