24

So I'm stumped. I know there's lots of Base64 encoders/decoders for JS, but not for the modified (and Facebook-favored) Base64URL variation. So far searching across stackoverflow has come up dry.

Yes, I could use PHP or another server-side library to decode this, but I'm trying to keep this universal regardless of what platform I'm using... for example, if I were to host a HTML-only Facebook app on Amazon S3/CloudFront and only use their JS SDK and jQuery to take care of processing forms and getting data.

That said, does anyone know of any Base64URL-specific decoders for JavaScript?

Thanks in advance!

2
  • 2
    Base64Url encoding is specified in RFC 4648, The Base16, Base32, and Base64 Data Encodings. The only difference between Base64 and Base64Url is two values (62 and 63). Just replace "+" with "-" and "/" with "_". Commented Jun 23, 2014 at 18:12
  • 2
    @jww would this be correct? var base64url = function(aStr) { return btoa(aStr.replace(/\+/g,'-').replace(/\//g,'_')).replace(/\=+$/m,'') } with the trialing ='s stripped? Commented May 11, 2016 at 22:01

10 Answers 10

54

Use this before decoding :

var decode = function(input) {
        // Replace non-url compatible chars with base64 standard chars
        input = input
            .replace(/-/g, '+')
            .replace(/_/g, '/');

        // Pad out with standard base64 required padding characters
        var pad = input.length % 4;
        if(pad) {
          if(pad === 1) {
            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
          }
          input += new Array(5-pad).join('=');
        }

        return input;
    }

After using this function you can use any base64 decoder

Sign up to request clarification or add additional context in comments.

2 Comments

Ah those times when you wish you can upvote twice :)
According to the Infra spec, atob removes the padding, so it's not necessary to add it. See infra.spec.whatwg.org/#forgiving-base64-decode
8

Using the TextEncoder interface along with the btoa() function and replace + with -, and / is replaced with _. Finally, remove trailing = characters added during base64 padding. For Decoding, the process is reversed.

Encode

function base64URLencode(str) {
  const utf8Arr = new TextEncoder().encode(str);
  const base64Encoded = btoa(utf8Arr);
  return base64Encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

Decode

function base64URLdecode(str) {
  const base64Encoded = str.replace(/-/g, '+').replace(/_/g, '/');
  const padding = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
  const base64WithPadding = base64Encoded + padding;
  return atob(base64WithPadding)
    .split('')
    .map(char => String.fromCharCode(char.charCodeAt(0)))
    .join('');
}

4 Comments

On the decode make sure to turn it back into a string with .join('')
@Ronnie Can you explain why the decoded string has to be split into an array, each element translated to charcode and back to a string, and then joined back into one string? Seems like returning the result of atob() is the same..
Yeah that's broken since Uint8Array gets silently converted/stringified to an array of comma-separated numbers in Firefox, don't use this solution
As noted by the MDN docs, this only works with strings that contain ASCII characters, or characters that can be represented by a single byte. In other words, this won't work with Unicode. See my answer below for an approach that works with Unicode.
3

Solution:

var b64str = base64.encode('foo bar');

// fix padding according to the new format
b64str = b64str.padRight(b64str.length + (4 - b64str.length % 4) % 4, '=');

Using this great base64 encode/decode: http://code.google.com/p/stringencoders/source/browse/trunk/javascript/base64.js

Also depends on the padRight method:

String.prototype.padRight = function(n, pad){
    t = this;
    if(n > this.length)
        for(i = 0; i < n-this.length; i++)
            t += pad;
    return t;
}

2 Comments

Hi Simeon... I think this could work, but I think I also need to replace "-" and "+" with "_" and "/" - good starting point though. I was hoping to find one with everything wrapped in the same library, but I guess I might just have to modify a library a little bit afterall.
Strange... actually, I just did a JS version of this answer: stackoverflow.com/questions/1228701/…
2

Though not widely available, you could use Uint8Array.prototype.toBase64():

function base64UrlEncode(str) {
  const bytes = new TextEncoder().encode(str)
  return bytes.toBase64({
    alphabet: 'base64url',
    omitPadding: true,
  })
}

Then use Uint8Array.fromBase64() to decode the base64url string:

function base64UrlDecode(str) {
  const bytes = Uint8Array.fromBase64(str, {
    alphabet: 'base64url',
    omitPadding: true,
  })
  return new TextDecoder().decode(bytes)
}

As mentioned this is not all that useful, since the API is marked as Limited Availability.

1 Comment

The decoding must be changed to Uint8Array with small i. I could not edit this as an edit must be at least 6 characters for whatever reason...
1

If you are using nodejs/webjs, and your expected output is a string with standard characters (from 0x00 to 0x7F):

function base64urlToStr (base64url) {
    const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
    return atob(base64);
};

If you are using webjs, and your expected output is a string with extended characters (from 0x00 to 0xFF):

function base64urlToBuf (base64url) {
    const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes;
};

If you are using nodejs, and your expected output is a string with extended characters (from 0x00 to 0xFF):

function base64urlToBuf (base64url) {
    return Buffer.from(base64url, 'base64url');
}

Mixing extended functions with standard strings will output the wrong result.

Comments

1

Same as Ronnie Smith's answer, but works for Unicode (i.e. strings that are not only ASCII characters):

function base64URLencode(str) {
  const bytes = new TextEncoder().encode(str)
  const binString = String.fromCodePoint(...bytes)
  const base64Encoded = btoa(binString)

  return base64Encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
function base64URLdecode(str) {
  const base64Encoded = str.replace(/-/g, '+').replace(/_/g, '/')
  const padding = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4))
  const base64 = base64Encoded + padding

  const binString = atob(base64);
  const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0));
  return new TextDecoder().decode(bytes)
}

(For the decode method, I use TextDecoder rather than the custom string concatenation.)

See also this article: https://web.dev/articles/base64-encoding

1 Comment

isn't the base64URLdecode function wrong? for str.length % 4 = 1, it will add '===', which is incorrect!
0

The answer by mohamed was really helpful, thanks!

If this happens for you: throw new Error("InvalidLengthError: Input base64url string is the wrong length to determine padding");

... you may be using it to decode a JSON Web Token (JWT).

But for this, you need to (after replacing the characters that need replacing) split apart the 3 parts of the JWT properly ("." character), and then pad each before attempting to decode using the code from that answer ( https://stackoverflow.com/a/51838635/1143126 ).

For a quick look inside your JWT, you could just use https://jwt.io/ if it's not critical to security (if it's just for a local test, for example).

Comments

0

Having wasted way too much time on the incomplete solutions above, here's a complete and working solution. No need for padding, no broken UTF8 arrays, no need to apply your own decoder.

function base64URLencode(str) {
  return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function base64URLdecode(str) {
  return atob(str.replace(/-/g, '+').replace(/_/g, '/'));
}

// Tests
console.log("Hi!", base64URLencode("Hi!"), base64URLdecode(base64URLencode("Hi!")));
console.log("Hello!", base64URLencode("Hello!"), base64URLdecode(base64URLencode("Hello!")));
console.log("Super secret key", base64URLencode("Super secret key"), base64URLdecode(base64URLencode("Super secret key")));

Comments

-1

I think the most efficient way of doing Base64/Base64URL decoding is to decode directly in a function instead of changing Base64URL into Base64 and then decoding it. I wrote a function that can decode both Base64 and Base64URL directly without any other dependency.

const PADCHAR = '=';
const B64index = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0,62,63,62,62,63,52,53,54,55,56,57,58,59,60,61, 0, 0,
    0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
  15,16,17,18,19,20,21,22,23,24,25, 0, 0, 0, 0,63, 0,26,27,28,
  29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,
  49,50,51];

function b64decode(s) {
  const len = s.length;
  if (len === 0) return s;
  const padding = (s.charAt(len-1) === PADCHAR);
  const pad = padding || ((len % 4) > 0);
  const L = (Math.floor((len+3)/4)-pad)*4;
  var x = [];
  for (i = 0; i < L; i += 4)
  {
    var n = B64index[s.charCodeAt(i)] << 18 | 
      B64index[s.charCodeAt(i+1)] << 12 | 
      B64index[s.charCodeAt(i+2)] << 6 | 
      B64index[s.charCodeAt(i+3)];
    x.push(String.fromCharCode(n >> 16, (n >> 8) & 0xff, n & 0xff));
  }
  if (pad)
  {
    var n = B64index[s.charCodeAt(L)] << 18 | 
      B64index[s.charCodeAt(L+1)] << 12;
    x.push(String.fromCharCode(n >> 16));
    if (len > L + 2 && ((s.charAt(L+2) != PADCHAR) || !padding))
    {
      n |= B64index[s.charCodeAt(L+2)] << 6;
      x.push(String.fromCharCode((n >> 8) & 0xff));
    }
  }
  return x.join('');
}

To use this function to decode the Base64URL string, I use JWT token as an example. First, I split the token. Then, I decode the JWT payload and then parse it into JSON object.

const elements = token.split('.');
const payload = JSON.parse(b64decode(elements[1]));

Comments

-10
var str = "string";
var encoded = btoa(str); // encode a string (base64)
var decoded = atob(encoded); //decode the string 
alert( ["string base64 encoded:",encoded,"\r\n", "string base64 decoded:",decoded].join('') );

1 Comment

This uses the normal, non-url-safe mapping charachters. See RFC 4648 §5 (en.wikipedia.org/wiki/Base64)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.