Base64 Encoder / Decoder ยท 5 min read
Base64 vs Base64URL: The Difference That Breaks JWTs
Base64 and Base64URL look almost identical but differ in three characters and the padding rule. Mixing them up is a common cause of broken JWTs and OAuth tokens.
A frustrating debugging session: you decode a JWT in your favourite Base64 tool, get gibberish, and start questioning your sanity. The token is fine. Your tool is wrong โ or rather, it's using the wrong variant. JWTs use Base64URL, not Base64. The two encodings differ in three characters and a padding rule, and that's enough to break decoders that don't know which one they're looking at.
The Two Alphabets
RFC 4648 defines both. Standard Base64 uses:
A-Z a-z 0-9 + /
Base64URL uses:
A-Z a-z 0-9 - _
Three substitutions: + becomes -, / becomes _, and the padding character = is usually omitted. Everything else is identical. Same 6-bit-per-character mapping, same 33% overhead, same decoding logic.
Why Two Variants Exist
Standard Base64 was designed for email (RFC 2045). Email doesn't care about +, /, or = โ they all pass through MIME unchanged. URLs do care:
+in a URL is interpreted as a space when decoding query strings (the legacyapplication/x-www-form-urlencodedrule)./is a path separator. A literal/inside what should be one token segment splits the path.=is the query string key/value delimiter. Embedded=characters confuse query parsers.
Any of those three would force percent-encoding to survive a URL โ turning a clean abc+def/gh== into abc%2Bdef%2Fgh%3D%3D, almost doubling the segment length. Base64URL sidesteps that by picking URL-safe characters from the start.
Where Each One Shows Up
Standard Base64 is used in MIME email attachments, PEM-encoded certificates, data URLs, HTTP Basic auth headers, and most JSON APIs that carry binary data inside a text field.
Base64URLshows up wherever the encoded value travels in a URL or a header that's parsed like a URL:
- JWTs (RFC 7519): all three segments โ header, payload, signature โ are Base64URL with no padding.
- JWS and JWE: the signature/encryption envelopes JWT is built on.
- OAuth 2.0 PKCE: the
code_verifierandcode_challengeare Base64URL. - WebAuthn / FIDO2: credential IDs, challenges, attestation responses.
- OpenID Connect: ID tokens and the various nonces and hashes inside them.
The Padding Rule
Standard Base64 always pads to a multiple of 4 characters with =. Base64URL is allowed to omit padding entirely โ the JWS spec (RFC 7515) requires it. So:
Standard Base64: TWFu (3 bytes "Man") Standard Base64: TWE= (2 bytes "Ma") Standard Base64: TQ== (1 byte "M") Base64URL (no pad): TWFu / TWE / TQ
A correct Base64URL decoder needs to re-add padding before passing the string to a standard Base64 routine. The fix is small but easy to forget: pad to a multiple of 4 with =, then translate - to + and _ to /.
How to Tell Them Apart
Look at the alphabet used:
- If the string contains
+or/or ends with=: standard Base64. - If the string contains
-or_and has no padding: Base64URL. - If it's all
A-Z a-z 0-9with no padding and no separators: ambiguous, but in any URL context assume Base64URL.
Most JWTs are easy to spot: three Base64URL segments separated by dots. The header almost always starts with eyJ โ that's the Base64 of {", the start of the JSON header.
The Common Bug
Decoding a JWT signature with a standard Base64 library throws "invalid character" on the first - or _. Or worse, it silently produces wrong bytes if the library translates unknown characters to zero. The signature verification then fails with no obvious cause โ the JSON parses, the claims look right, but the cryptographic check is comparing two unrelated byte strings.
Use a Base64URL-aware decoder for anything inside a JWT, OAuth flow, or WebAuthn response. Most modern languages ship one (base64.urlsafe_b64decode in Python, Buffer.from(s, 'base64url') in Node 16+, java.util.Base64.getUrlDecoder() in Java). When in doubt, normalise the input โ replace - with + and _ with /, pad with =, then decode as standard Base64.
Two encodings, three characters of difference, one common bug. Knowing which is which saves an afternoon.
References
- Josefsson, S. (2006). RFC 4648: The Base16, Base32, and Base64 Data Encodings. Internet Engineering Task Force.
- Jones, M., Bradley, J., & Sakimura, N. (2015). RFC 7519: JSON Web Token (JWT). Internet Engineering Task Force.
- Jones, M. (2015). RFC 7515: JSON Web Signature (JWS). Internet Engineering Task Force.
- Hardt, D. (2012). RFC 6749: The OAuth 2.0 Authorization Framework. Internet Engineering Task Force.