Credentials in URLs
I recently finished reading âThe History of the URLâ over on Cloudflareâs blog. It was a good read, full of interesting detailsâsome that I understood, some that I did not.
One of the things that stood out to me was this little nugget:
As you may know, you can include a username and password in URLs:
http://zack:shhhhhh@zack.is
Oh hey, I knew that. I havenât consciously used that functionality in a long time. The last time I remember putting username and password in the URL itself was maybe in high school when I first started learning about web sites. Itâs one of those things I knew you could do, but have never really done purposefully. Cloudflare continues:
The browser then encodes this authentication data into Base64, and sends it as a header:
Authentication: Basic emFjazpzaGhoaGho
Wait, what? I didnât know thatâs how it works. Wait, so does that mean I could write a fetch
request and, instead of setting the Authorization
header, I could conceivably put the username and password directly into the URL and the browser would set the header for me?
fetch(`https://user:pass@jsonplaceholder.typicode.com/todos/1`)
Turns out, no. fetch
doesnât even support passing credentials via the URL. It sounds like it was specifically designed not to support that case. If you try to do it, the browser will throw an error.
This got me thinking, I wonder if it works with the old XMLHttpRequest
? Apparently, as of this writing in Chrome and Safari on Mac, it does. Kind of. What I mean is, it will still make the network call, unlike fetch
.
const req = new XMLHttpRequest();
req.open(
"GET",
"https://user:pass@jsonplaceholder.typicode.com/todos/1"
);
req.onload = res => {
console.log(req.response);
};
req.send();
That request gets made but the browser doesnât pass along the credentials as an Authorization
header. The credentials just get lost. You can check out my codepen for sending credentials in the URL for a complete example of this in the browser.
So what exactly is going on here? fetch
fails if you try to include creds in the URL while XMLHttpRequest
still makes the request but wonât translate the creds in the URL to the appropriate Authorization
header.
Neil Madden has an aptly-named blog post titled âCan you ever (safely) include credentials in a URL?â where he enumerates all the reasons putting credentials in the URL was not and is not the best idea. (like, for example, when people realized a valid URL like http://www.google.com:search@example.com was actually perfect for phishing attacks). He summarizes:
this specific form of URL was deprecated back in 2005, and now support within browsers is patchy: Safari for instance will just silently ignore any username:password component when following such a link. Other browsers will tolerate them in some cases, but this varies considerably by browser and often by version to version. Some versions of Chrome refuse to follow such links and instead display a large red phishing warning page.
Now Iâm beginning to more fully understand why itâs been so long since I tried to encode credentials into a URL in any useful way.
While weâre on the topic of encoding credentials into a URL, there was another part from the Cloudflare blog post that stood out to me:
The only reason for the Base64 encoding is to allow characters which might not be valid in a header, it provides no obscurity to the username and password values.
Again, Iâve worked on the web for a long time, and I never knew this. I always sort of assumed we used base64 to add at least some kind of obfuscation to the plain text username:password
. In my mind I imagined the creators behind the standard to have had a conversation where they figured âok, well this is going to be over HTTP and to prevent reading the username/password at a glance, letâs provide some kind of âsecurity through obscurityâ and base64 encode the thing.â Wrong. Apparently itâs to provide encoding of potentially invalid characters. From Wikipediaâs entry on Basic access authentication:
While encoding the user name and password with the Base64 algorithm typically makes them unreadable by the naked eye, they are as easily decoded as they are encoded. Security is not the intent of the encoding step. Rather, the intent of the encoding is to encode non-HTTP-compatible characters that may be in the user name or password into those that are HTTP-compatible.
Well look at that: you learn something new every day huh? Iâm going to start a new tag on my blog to document things like thisâthings that I probably shouldâve learned a long time ago and am embarrassed to admit that Iâve only learned just now. Iâll call it: #theMoreYouKnow.