GoWin Tools
Tools
โ† Base64 Encoder / Decoder

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 legacy application/x-www-form-urlencoded rule).
  • / 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_verifier and code_challenge are 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-9 with 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

  1. Josefsson, S. (2006). RFC 4648: The Base16, Base32, and Base64 Data Encodings. Internet Engineering Task Force.
  2. Jones, M., Bradley, J., & Sakimura, N. (2015). RFC 7519: JSON Web Token (JWT). Internet Engineering Task Force.
  3. Jones, M. (2015). RFC 7515: JSON Web Signature (JWS). Internet Engineering Task Force.
  4. Hardt, D. (2012). RFC 6749: The OAuth 2.0 Authorization Framework. Internet Engineering Task Force.