URL-Safe Base64 Encoding: Why and How
Standard Base64 uses three characters that silently corrupt data when placed in a URL: +, /, and =. URL safe Base64 (Base64url) fixes this with a simple alphabet swap. Here is everything you need to know.
The Problem: Three Characters That Break URLs
Standard Base64 was defined in the 1980s for encoding binary data in email. The 64-character alphabet it uses works perfectly in email bodies, but causes serious problems in URLs:
+(plus): In URL query strings,+is interpreted as a space character. A Base64 string likeabc+defbecomesabc defafter URL parsing, silently corrupting your data./(slash): The forward slash is a path separator in URLs. If a Base64 string appears in a URL path segment and contains/, the server may interpret everything after it as a new path component.=(equals, padding): The equals sign is a key-value separator in query strings. A padded Base64 value likeabc==in a query string can confuse parsers and is sometimes stripped entirely.
Here is a concrete example of what goes wrong. Suppose you encode a security token and put it in a URL:
// Standard Base64
const token = btoa('\xfb\xff\xfe'); // "+/+/" ← contains + and /
const url = `https://example.com/verify?token=${token}`;
// Result: https://example.com/verify?token=+/+/
// After URL parsing the server receives token=" / /" ← CORRUPTED
This is a real security issue. Password reset tokens, email verification links, and OAuth state parameters all get passed through URLs. If your encoding produces a + or /, the token gets corrupted and the link fails for the subset of users whose token happens to include those characters.
The Solution: Base64url (RFC 4648 Section 5)
Base64url is defined in RFC 4648, Section 5. It is identical to standard Base64 except for two character substitutions and optional removal of padding:
Standard Base64 alphabet:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
Base64url alphabet:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
^^
+ becomes - / becomes _
The padding character = is also typically omitted in URL contexts because the length of a Base64url string can always be used to infer how much padding would be needed, making it redundant.
With this alphabet, a Base64url string contains only characters in the set [A-Za-z0-9\-_], which are all URL safe and require no percent-encoding.
Where Base64url Is Used
Base64url is not just an academic exercise - it is embedded in some of the most widely used security protocols on the internet:
- JSON Web Tokens (JWT): All three parts of a JWT (header, payload, signature) are Base64url encoded without padding. This is defined in RFC 7519.
- OAuth 2.0 PKCE: The code challenge in the PKCE flow is a Base64url-encoded SHA-256 hash of the code verifier.
- WebAuthn / FIDO2: Credential IDs and public key data are transmitted as Base64url strings.
- URL parameters for binary tokens: Password reset tokens, email verification links, and invitation codes should always use Base64url to be link-safe.
- HTTP cookies: While cookies do not parse
+as a space, using Base64url avoids any ambiguity with the=attribute separator.
Every JWT you have ever used contains Base64url encoding. The three dot-separated parts of
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signatureare each independently Base64url-decoded to read the algorithm, claims, and verify the signature.
Step-by-Step: Converting Between Standard Base64 and Base64url
The conversion is a simple character replacement that can be done in any language:
JavaScript (Browser and Node.js)
// Standard Base64 → Base64url
function toBase64Url(base64) {
return base64
.replace(/\+/g, '-') // + becomes -
.replace(/\//g, '_') // / becomes _
.replace(/=+$/, ''); // strip padding
}
// Base64url → Standard Base64
function fromBase64Url(base64url) {
// Restore padding
const padded = base64url + '==='.slice((base64url.length + 3) % 4);
return padded
.replace(/-/g, '+')
.replace(/_/g, '/');
}
// Encode a string directly to Base64url
function encodeBase64Url(str) {
return toBase64Url(btoa(unescape(encodeURIComponent(str))));
}
// Decode a Base64url string
function decodeBase64Url(str) {
return decodeURIComponent(escape(atob(fromBase64Url(str))));
}
Node.js 14.18+ (Built-in Support)
// Node.js has native base64url support via Buffer
const encoded = Buffer.from('Hello World').toString('base64url');
// "SGVsbG8gV29ybGQ" ← no padding, URL safe characters
const decoded = Buffer.from('SGVsbG8gV29ybGQ', 'base64url').toString();
// "Hello World"
Python
import base64
# Encode to Base64url (no padding)
def encode_base64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')
# Decode from Base64url (add padding back)
def decode_base64url(s: str) -> bytes:
padding = 4 - len(s) % 4
if padding != 4:
s += '=' * padding
return base64.urlsafe_b64decode(s)
Go
import "encoding/base64"
// Go's standard library has dedicated URL safe Base64
encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("Hello"))
decoded, _ := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(encoded)
Encode and Decode Base64url Online
Our free Base64 tool supports both standard and URL safe encoding modes. 100% client side - your data never leaves the browser.
Open Base64 Tool →Decoding a JWT Manually
Since JWTs are Base64url-encoded, you can decode the header and payload without any library:
function decodeJwtPayload(jwt) {
// JWT format: header.payload.signature
const parts = jwt.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT format');
// Decode the payload (part index 1)
const base64url = parts[1];
// Restore standard Base64 padding and characters
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
// Decode and parse JSON
return JSON.parse(atob(padded));
}
// Example
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZXhwIjoxNzQwMDAwMDAwfQ.signature';
console.log(decodeJwtPayload(jwt));
// { sub: "user123", exp: 1740000000 }
Important: this decodes the payload but does not verify the signature. Never trust the claims in a JWT without verifying the signature using the correct secret or public key.
Padding: Required or Optional?
Standard Base64 always pads the output to a multiple of 4 characters using =. Base64url padding is optional because the receiver can calculate the required padding from the string length:
// Padding calculation
function addPadding(base64url) {
const mod4 = base64url.length % 4;
if (mod4 === 0) return base64url; // no padding needed
if (mod4 === 2) return base64url + '=='; // need 2 padding chars
if (mod4 === 3) return base64url + '='; // need 1 padding char
throw new Error('Invalid Base64url string length');
// mod4 === 1 is always invalid Base64
}
Most Base64url implementations strip padding on encode and restore it on decode. When in doubt, strip padding when storing or transmitting, and restore before decoding.
Common Mistakes
- Forgetting to handle padding when decoding: If your Base64url decoder throws "Invalid character" or produces garbled output, the most likely cause is missing padding. Always restore padding before calling a standard Base64 decoder.
- Mixing standard and URL safe: A Base64 string decoded with the wrong alphabet produces corrupted bytes silently. Be explicit about which variant you are using.
- Double-encoding in URLs: Some developers Base64url-encode a value and then also percent-encode it (
-becomes%2D). This is unnecessary because Base64url characters are already URL safe. Avoid double-encoding. - Using standard Base64 in JWT libraries: JWT libraries use Base64url, not standard Base64. If you try to verify a JWT using a standard Base64 decoder, the signature check will fail for tokens that contain
+or/characters in their standard Base64 equivalent.
FAQ
Is Base64url the same as Base64?
Almost. Base64url uses the same algorithm but a different 64-character alphabet: - instead of + and _ instead of /. Padding (=) is also typically omitted. The encoding and decoding logic is otherwise identical.
Do I need Base64url for cookies?
Standard Base64 is technically safe in cookie values as long as you do not use it in the cookie name. However, using Base64url is best practice because it avoids any ambiguity with the = attribute separator syntax in cookie headers.
Why does my JWT have three parts separated by dots?
A JWT consists of three Base64url-encoded JSON objects: the header (algorithm info), the payload (claims), and the signature. They are joined with . as a delimiter. The signature is computed over the first two parts and verifies they have not been tampered with.
Why does the same data produce a different Base64url string each time for JWTs?
If you are using an asymmetric algorithm like RS256, the signature is deterministic, and the same payload will produce the same JWT. However, if additional claims like iat (issued at) or jti (JWT ID) are included and change per token, the output will differ each time.
Can I use URL safe Base64 in HTML attributes?
Yes. Base64url strings contain only alphanumeric characters plus - and _, which are all safe in HTML attribute values without quoting. Standard Base64 with +, /, and = is also technically safe in HTML attributes but may cause issues in certain contexts like inline event handlers.
Use our free tool here → Base64 Encoder / Decoder on SecureBin.ai
Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.