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
Sylvain
  • 19,099
  • 23
  • 96
  • 145
  • Could you please post the sent request and response headers for the `/api/login` page and the image? – Bergi Dec 20 '13 at 14:31
  • 1
    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`? – quietmint Dec 20 '13 at 14:31
  • @user113215 : I don't want to put the password in the source. – Sylvain Dec 20 '13 at 14:33
  • 1
    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`? – Khanh TO Dec 20 '13 at 14:36
  • 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. – Sylvain Dec 20 '13 at 14:43
  • @Bergi : I've added the requests' detail in my question. – Sylvain Dec 20 '13 at 14:56
  • Do you image have relative or absolute src? – STO Dec 20 '13 at 15:13
  • STO, it's a relative src (`../api/user/picture/2218`) – Sylvain Dec 20 '13 at 15:39
  • 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 `` tag (`http://user:pass@server`)? – ComFreek Dec 20 '13 at 16:28
  • 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 `` (not through REST) but still need some sort of authentication – Reza S Dec 20 '13 at 16:30
  • @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. – Sylvain Dec 20 '13 at 16:32
  • @Sylvain 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'. – ComFreek Dec 20 '13 at 16:35
  • @Sylvain 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 pass – Paul Scheltema Dec 20 '13 at 16:55
  • @PaulScheltema : This is running on https only so the credentials in the headers are safe. – Sylvain Dec 20 '13 at 17:00
  • @ComFreek : 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. – Sylvain Dec 20 '13 at 17:04
  • @Sylvain https or not, the headers send are still visible to the user of the browser, theyre just encoded over the wire – Paul Scheltema Dec 20 '13 at 17:06
  • @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. – Sylvain Dec 20 '13 at 17:09
  • @Sylvain 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 servers – Paul Scheltema Dec 20 '13 at 17:13
  • @PaulScheltema : What do you mean by "one resource with the credentials"? – Sylvain Dec 20 '13 at 17:16
  • 1
    @Sylvain in the html source: – Paul Scheltema Dec 20 '13 at 17:20
  • @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 (http://stackoverflow.com/questions/3823357/how-to-set-the-img-tag-with-basic-authentication) it is said that not all browsers support this syntax) – Sylvain Dec 20 '13 at 17:33
  • 1
    +1 @PaulScheltema. As an improvement, no need to include `` 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. – matpop Dec 20 '13 at 17:37
  • @Sylvain you could load all images with XHR http://jsperf.com/encoding-xhr-image-data/14 – Paul Scheltema Dec 20 '13 at 18:13
  • @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. – Sylvain Dec 20 '13 at 18:26
  • @Sylvain 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 know – Paul Scheltema Dec 20 '13 at 18:39
  • 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. – Sylvain Dec 20 '13 at 18:44
  • Do the images need to be secured? If not, maybe the easiest solution would be to serve them separately without the need for authentication... – Pieter Herroelen Dec 23 '13 at 09:11
  • @PieterHerroelen, they need to be secured. – Sylvain Dec 23 '13 at 15:18

7 Answers7

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.

ComFreek
  • 29,044
  • 18
  • 104
  • 156
  • 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). – Sylvain Dec 24 '13 at 18:58
  • @PaulScheltema had suggested this approach: http://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. – Sylvain Dec 24 '13 at 19:02
  • Also, this approch is not going to work on IE8, `window.bota` is only supported with IE11. – Sylvain Dec 24 '13 at 19:03
  • @Sylvain Have a look at this question and especially its accepted answer: http://stackoverflow.com/questions/1095102/how-do-i-load-binary-image-data-using-javascript-and-xmlhttprequest. It's easier to base64-encode the image on the server side. Is that possible in your environment? – ComFreek Dec 27 '13 at 11:23
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?

Community
  • 1
  • 1
flup
  • 26,937
  • 7
  • 52
  • 74
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.

SzabV
  • 244
  • 1
  • 6
  • 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. – Sylvain Dec 24 '13 at 19:05
  • How about if you create a session on your proxy, does the cookie header get sent? – flup Dec 26 '13 at 09:17
  • 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. – SzabV Dec 27 '13 at 22:29
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.

Jonas
  • 1,692
  • 13
  • 17
  • How woud you authorize a request "automatically"? – flup Feb 09 '14 at 15:19
  • 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. – Jonas Feb 10 '14 at 21:29
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"

Sergey Pekar
  • 8,555
  • 7
  • 47
  • 54
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.

Aziz Shaikh
  • 16,245
  • 11
  • 62
  • 79
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.

Franklin Yu
  • 8,920
  • 6
  • 43
  • 57
  • 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). – Ján Lalinský Mar 10 '19 at 20:16