Why doesn't the browser reuse the authorization headers after an authenticated XMLHttpRequest?
Asked Answered
O

7

42

I'm developing Single Page App using Angular. The backend exposes REST services that require Basic authentication. Getting index.html or any of the scripts does not require authentication.

I have an odd situation where one of my view has a <img> where the src is the url of a REST API that requires authentication. The <img> is processed by the browser and I have no chance to set the authorization header for GET request it makes. That causes the browser to prompt for credentials.

I attempted to fix this by doing this:

  1. Leave img src empty in the source
  2. At "document ready", make an XMLHttpRequest to a service (/api/login) with the Authorization header, just to cause the authentication to occur.
  3. Upon completing that call, set the img src attribute, thinking that by then, the browser would know to include the Authorization header in subsequent requests...

...but it doesn't. The request for the image goes out without the headers. If I enter the credentials, then all other images on the page are right. (I've also tried and Angular's ng-src but that produced the same result)

I have two questions:

  1. Why didn't the browser (IE10) include the headers in all requests after a successful XMLHttpRequest?
  2. What can I do to work around this problem?

@bergi asked for requests' details. Here they are.

Request to /api/login

GET https://myserver/dev30281_WebServices/api/login HTTP/1.1
Accept: */*
Authorization: Basic <header here>
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)
Connection: Keep-Alive

Response (/api/login)

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 4
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 20 Dec 2013 14:44:52 GMT

Request to /user/picture/2218:

GET https://myserver/dev30281_WebServices/api/user/picture/2218 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)
Connection: Keep-Alive

And then the web browser prompts for credentials. If I enter them, I get this response:

HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Length: 3119
Content-Type: image/png
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 20 Dec 2013 14:50:17 GMT
Owe answered 16/12, 2013 at 18:5 Comment(28)
Could you please post the sent request and response headers for the /api/login page and the image?Unpeopled
If you're using HTTP authentication, can't you just manipulate the src URLs to include the username/password like http://user:pass@server/path/to/img.png?Lateral
@user113215 : I don't want to put the password in the source.Owe
You don't want to put the password in the source. Where do you store the password on browser (to include in XMLHttpRequest) and hidden from view?Incommode
Sorry, I forgot to explain that part. I'm controlling the browser through a WebBrowser control hosted in an application written in C#. The C# app loads the browser at index.html and passes the credentials to my Angular app using the javascript invocation capabilities provided by the WebBrowser control. Then the angular app sets these as default headers on $http.Owe
@Unpeopled : I've added the requests' detail in my question.Owe
Do you image have relative or absolute src?Transcendentalism
STO, it's a relative src (../api/user/picture/2218)Owe
How should the password be entered? Can't you inject the password from C# into the JS environment which then sets the correct URI for the <img> tag (http://user:pass@server)?Fluoride
I believe it's because of the fact that not necessarily all your browser are actually REST calls. Your browser can't make that decision for you and you have to manually ad the Http-Authorization header. But I'm facing the same problem as I'm serving images static <img src='url/img.png'> (not through REST) but still need some sort of authenticationBroom
@ComFreek: I don't want to burn the password in the markup. A user would be very scared to right-click on an image to copy its path and see his password as clear text in the link.Owe
@Owe What about loading the images in JS and injecting them base64-encoded in the markup? One downside if of course that the user will see a long URI when right-clicking the image and selecting 'Image properties'.Fluoride
@Owe its basic auth so the user/pass will always be visible in the headers send, cant you just include one image 1x1 in the source with the user/pass in the url, once the realm is authenticated your other request should just passSkylark
@PaulScheltema : This is running on https only so the credentials in the headers are safe.Owe
@Fluoride : Loading the images in JS and injecting them base64-encoded could work I suppose. Are there performance related concerns with that approach? If you post a complete answer explaining how to do that I might accept that answer.Owe
@Owe https or not, the headers send are still visible to the user of the browser, theyre just encoded over the wireSkylark
@PaulScheltema. Indeed, I misinterpreted your comment. I don't think the user cares about headers. I just don't want him the share a link to his picture taken on my system, and unknowingly share his credentials by doing that.Owe
@Owe ok back to my awnser then, put one resource with the credentials in the source and you should be set, i use the same for our dev serversSkylark
@PaulScheltema : What do you mean by "one resource with the credentials"?Owe
@Owe in the html source: <img src="user:pass@server/1x1.gif">Skylark
@PaulScheltema : ho, I see. I'm still not conformable burning credentials in the page though. But that would work (assuming IE supports this because here (#3823857) it is said that not all browsers support this syntax)Owe
+1 @PaulScheltema. As an improvement, no need to include <img src="user:pass@server/1x1.gif"> in the html source. It can be appended dynamically via script and then removed as soon as it's loaded, even better with visibility: hidden or equivalent.Residential
@Owe you could load all images with XHR jsperf.com/encoding-xhr-image-data/14Skylark
@PaulScheltema : Thanks for the reference. That's the same solution as what ComFreek suggested, right? I think I'm going to do it that way.Owe
@Owe no, he suggested you put the base64-encoded image data in your html/js, eg. on the server read all images, convert to bas64, and render them in the template. Either sollution works, there are some other sollutions also, like proxying. Just depends on where you want to solve it, however as to the question of why, i wouldnt knowSkylark
A workaround would be to make index.html a resource that requires authorization (even though there is need to secure it). That would resolve the issue without anything special.Owe
Do the images need to be secured? If not, maybe the easiest solution would be to serve them separately without the need for authentication...Insole
@PieterHerroelen, they need to be secured.Owe
F
24

Basic idea

Load the images via JavaScript and display them on the site. The advantage is that the authentication credentials will never find their way into the HTML. They will resist at the JavaScript side.

Step 1: load the image data via JS

That's basic AJAX functionality (see also XMLHttpRequest::open(method, uri, async, user, pw)):

var xhr = new XMLHttpRequest();
xhr.open("GET", "your-server-path-to-image", true, "username", "password");

xhr.onload = function(evt) {
  if (this.status == 200) {
    // ...
  }
};

Step 2: format the data

Now, how can we display the image data? When using HTML, one would normally assign an URI to the src attribute of the image element. We can apply the same principle here except for the fact that we use data URIs instead of 'normal' http(s):// derivates.

xhr.onload = function(evt) {
  if (this.status == 200) {
    var b64 = utf8_to_b64(this.responseText);
    var dataUri = 'data:image/png;base64,' + b64; // Assuming a PNG image

    myImgElement.src = dataUri;
  }
};

// From MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/window.btoa
function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

Canvas

There is also another option which consists in painting the loaded data in a <canvas> field. This way, the user won't be able to right-click the image (the area where the canvas is positioned) as opposed to the <img> and data URIs where the user will see a long data URI when viewing the image properties panel.

Fluoride answered 20/12, 2013 at 20:36 Comment(4)
I tried to make this work in Chrome to test it but no image appear. I get a result from the utf8_to_b64() function, but assigning it to the image does not work (and it is a PNG).Owe
@PaulScheltema had suggested this approach: jsperf.com/encoding-xhr-image-data/14. I made it work in Chrome but not in IE10. IE10 does not support overrideMimeType (of XMLHttpRequest). IE11 supports it though. But I did not try it on IE11 as I need this to work with IE8 and up.Owe
Also, this approch is not going to work on IE8, window.bota is only supported with IE11.Owe
@Owe Have a look at this question and especially its accepted answer: #1095602. It's easier to base64-encode the image on the server side. Is that possible in your environment?Fluoride
P
9

The google drive uploader is created using angular js. Its authors faced a similar problem. The icons were hosted on a different domain and putting them as img src= violated the CSP. So, like you, they had to fetch the icon images using XHR and then somehow manage to get them into the img tags.

They describe how they solved it. After fetching the image using XHR, they write it to the HTML5 local file system. They put its URL on the local file system in the img's src attribute using the ng-src directive.

$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
  console.log('Fetched icon via XHR');
  blob.name = doc.iconFilename; // Add icon filename to blob.
  writeFile(blob); // Write is async, but that's ok.
  doc.icon = window.URL.createObjectURL(blob);
  ...
}

As for the why, I don't know. I assume that creating a session token for retrieving the images is out of the question? I'd expect that Cookie headers do get sent? Is it a cross-origin request? In that case, do you set the withCredentials property? Is it a P3P thing perhaps?

Pipistrelle answered 24/12, 2013 at 2:15 Comment(2)
HTML5, that's not going to work with IE. I have to support IE8 and up.Owe
IE 10 and up for blob urls, I'm afraid, yesPipistrelle
A
4

Another approach would be to add an end point to your sites back end that proxied the image request. So your page could request it without credentials and the back end would take care of the authentication. The back end could also cache the image if it didn't change frequently or you knew the frequency with which it was updated. This is fairly easy to do on the back end, makes your front end simple and prevents credentials being sent to the browser.

If the issue is authentication then the links could contain a single use token generated for the user that is authenticated and only accessible from their current browser session. Giving secure access to the content only for the user it was intended for and only for the time they are authorized to access it. This would also require work in the back end, however.

Arly answered 20/12, 2013 at 20:53 Comment(3)
I cannot do that, credentials are different for each end-user and I need to authorize each request. One user might not have the permissions to view another user's picture.Owe
How about if you create a session on your proxy, does the cookie header get sent?Pipistrelle
What you would be doing on the backed is crafting an HTTP request that would allow the retrieval of the required image file. You can certainly add a cookie to this request as you are in full control of it.Arly
F
2

It seems to me that to solve your problem you should change the design of your app, instead of trying to hack your way around how browsers actually work.

A request to a secure URL will always need authentication, regarding of it being done by the browser with an img tag or in javascript.

If you can perform authorization automatically without user interaction, you can do it on the server side and you don't need to send any user+pass to the client to do this. If that is the case, you could change the code behind https://myserver/dev30281_WebServices/api/user/picture/2218 to perform the authorization and serve the image, without HTTP auth, only if the user is authorized to request it, otherwise return a 403 forbidden response (http://en.wikipedia.org/wiki/HTTP_403).

Another possible solution would be separate the pages that include the secure images from the rest of the app. So you would theoretically have two single-page-apps. The user would be required to login to access the secure part. I'm not sure though if this is possible in your case, since you didn't state all requirements. But it makes more sense that if you want to serve secure resources that require authentication, that the user should be prompted for credentials, just as the browser does.

Filipino answered 25/12, 2013 at 14:35 Comment(2)
How woud you authorize a request "automatically"?Pipistrelle
By "automatically", I meant that you have server side code which can handle the authorization with stored user information from the authentication. It doesn't require any user interaction to do it.Filipino
E
2

I always parse

Set-Cookie header value in previous (or first login request) and then send it's value in next requests.

Something like this

Response after first request:

Date:Thu, 26 Dec 2013 16:20:53 GMT
Expires:-1
Pragma:no-cache
Set-Cookie:ASP.NET_SessionId=lb1nbxeyfhl5suii2hfchxpx; domain=.example.com; path=/; secure; HttpOnly
Vary:Accept-Encoding
X-Cdn:Served-By-Akamai
X-Powered-By:ASP.NET

Any next request:

Accept:text/html,application/xhtml+xml
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8,ru;q=0.6
Cache-Control:no-cache
Connection:keep-alive
Cookie:ASP.NET_SessionId=lb1nbxeyfhl5suii2hfchxpx;

As you can see I send ASP.NET_SessionId="any value" value in Cookie header. If server uses php you should parse PHPSESSID="some value"

Edomite answered 26/12, 2013 at 16:28 Comment(1)
Hi, I don't (want to) use sessions on the server.Owe
G
2

You need to try using the Access-Control-Allow-Credentials: true header. I once encountered an issue with IE which eventually boiled down to the use of this header. Also set $httpProvider.defaults.headers.get = { 'withCredentials' : 'true' } in the angular js code.

Gillispie answered 27/12, 2013 at 12:35 Comment(0)
S
0

As for the reason: I tried Chrome and Firefox, and both remember basic authorization only if the credential is entered directly from Browser UI, i.e. the pop-up made by browser. It will not remember it if the credential came from JavaScript, although the HTTP request is the same. I guess this is by design, but I don't see it mentioned in standard.

Steels answered 16/1, 2019 at 20:9 Comment(1)
This is not true in general, I've checked this with current Firefox(65) and Chromium(72) and both do remember credentials for domain even if sent initially from Javascript (using XMLHttpRequest).Biplane

© 2022 - 2024 — McMap. All rights reserved.