What Base64 is for
Base64 turns any sequence of bytes into text using 64 printable ASCII characters, three bytes at a time into four characters. It exists so binary data can travel through systems designed for text: email bodies, JSON strings, HTTP headers, XML. It is not encryption and it is not compression; the output is a third larger than the input and anyone can reverse it.
The two alphabets
| Standard Base64 (RFC 4648 §4) | Base64url (RFC 4648 §5) | |
|---|---|---|
| Characters 0 to 61 | A-Z a-z 0-9 | A-Z a-z 0-9 |
| Character 62 | + | - |
| Character 63 | / | _ |
| Padding | = to a multiple of 4 | Usually omitted |
| Used by | MIME email, Authorization: Basic, data URIs, PEM certificates, most APIs and config files | JWT, WebAuthn, OAuth PKCE, most things that put a value in a URL or a cookie |
The reason for the second alphabet is in the name. +, / and = all have meanings in a URL: + is a space in query strings, / separates path segments, = separates keys from values. A standard Base64 value dropped into a URL either gets mangled or has to be percent-encoded (%2B%2F%3D), which is ugly and error-prone. Base64url swaps in - and _, which are safe everywhere, and drops the padding because the length tells the decoder how many bytes to expect anyway.
What goes wrong
Decoding Base64url with a standard decoder
The decoder meets - or _, which are not in its alphabet, and fails: Invalid character, illegal base64 data at input byte N, Incorrect padding. This is the classic JWT decoding error. Every segment of a JWT is Base64url without padding, and atob() in a browser, base64.b64decode in Python and base64.StdEncoding in Go are all standard decoders. The JWT decoder handles this for you; if you are decoding by hand, use the Base64 encoder/decoder with the scheme set to Base64url.
Padding
Standard Base64 output is always a multiple of four characters, padded with one or two =. Base64url usually has none. Strict standard decoders reject unpadded input (Incorrect padding); some lenient ones accept it. Strict Base64url decoders reject padded input. To convert, add = until the length is a multiple of four, or strip them.
Standard Base64 in a URL
A + in a query parameter arrives at the server as a space. A / may be interpreted as a path separator. The decoder sees a space or a truncated string and fails, but only for values that happened to contain a 62 or 63, so it works most of the time and fails intermittently. If a value ever goes near a URL, use Base64url or percent-encode it.
Line wrapping
MIME Base64 (RFC 2045) inserts a line break every 76 characters, and openssl base64 does the same every 64. Decoders that do not expect newlines fail. Strip all whitespace before decoding; the codec on this site does so automatically.
Converting between them
# standard to url-safe
tr '+/' '-_' <<< "$B64" | tr -d '='
# url-safe to standard (then pad)
s=$(tr '-_' '+/' <<< "$B64URL"); while (( ${#s} % 4 )); do s+="="; done; echo "$s"
| Language | Base64url decode |
|---|---|
| JavaScript | atob(s.replace(/-/g,'+').replace(/_/g,'/').padEnd(Math.ceil(s.length/4)*4,'=')) |
| Python | base64.urlsafe_b64decode(s + '=' * (-len(s) % 4)) |
| Go | base64.RawURLEncoding.DecodeString(s) |
| Java | Base64.getUrlDecoder().decode(s) |
| PHP | base64_decode(strtr($s, '-_', '+/')) |
| Ruby | Base64.urlsafe_decode64(s) |
Go's Raw encodings are the ones without padding; URLEncoding alone still expects =. Python's urlsafe_b64decode handles the alphabet but still wants padding, hence the arithmetic.
Which to use
If the value will ever appear in a URL, a cookie, a filename or a JWT: Base64url, no padding. If it is going into an email, a PEM file, a Basic auth header or an API that documents "Base64": standard, with padding. If you are decoding something someone else produced, look at the characters: - or _ means url-safe, + or / means standard, and trailing = means padded. Neither one is more secure than the other, because neither one is secure at all; they are encodings, and anything encoded can be decoded by anyone.