Base64 vs. Base64url: Why JWTs Use a Different Alphabet
· 2 min read
Base64 exists to smuggle binary data through systems that only speak text. It maps every 3 bytes of input onto 4 characters from a 64-character alphabet — letters, digits, and two symbols. The problem: the standard alphabet's two symbols are + and /, and its padding character is =. All three are landmines inside a URL.
Where standard Base64 breaks
Put a+b/c9== in a query string and three things go wrong at once:
+decodes to a space in form-encoded contexts, soa+barrives asa b. The data is silently corrupted — no error, just a signature that won't verify./is the path separator. In a path segment,/splits your token into two segments and probably 404s.=separates keys from values in query strings, confusing parsers that see?token=a+b/c9==.
You can percent-encode your way around this (%2B, %2F, %3D), but now the token is longer, uglier, and will be mangled by any layer that double-encodes.
The base64url fix
RFC 4648 §5 defines a URL-safe variant with two character swaps and one deletion:
| Standard | base64url | |
|---|---|---|
| 62nd character | + |
- |
| 63rd character | / |
_ |
| Padding | = |
omitted |
Hyphen and underscore are both unreserved URL characters — safe anywhere, never encoded. And the padding turns out to be redundant: the payload length is recoverable from the string length (length % 4 tells you how many bytes the final group held), so base64url simply drops the =.
Standard: eyJzdWIiOiI0MiJ9+A/w==
base64url: eyJzdWIiOiI0MiJ9-A_w
Why JWTs made base64url famous
A JSON Web Token is three base64url segments joined by dots:
header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTQyIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9P
JWTs travel in URLs (password-reset links, magic links), in headers, and in cookies — three contexts where +/= would each cause distinct breakage. The JOSE specifications therefore mandate base64url without padding for every segment. The same reasoning applies anywhere data rides in a URL: OAuth state values, pagination cursors, share tokens, webhook signature parameters.
One thing base64url does not do: encrypt. A JWT payload is encoded, not protected — anyone can decode it. Never put secrets in one; the signature prevents tampering, not reading.
Converting between the two
The conversion is mechanical — swap the two characters, add or strip padding:
Standard → URL-safe: replace + with -, / with _, delete trailing =.
URL-safe → standard: replace - with +, _ with /, append = until the length is a multiple of 4.
The classic bug lives in that second direction: many standard-library decoders reject unpadded input, so a raw JWT segment fed to a strict Base64 decoder throws "invalid length". If a token looks like Base64 but won't decode, missing padding is the first thing to check.
The Base64 URL-safe tool handles all of it — encoding text (full Unicode) to either flavor, decoding either flavor back, and converting between them with auto-detection of which alphabet you pasted.