25

I'm building an AngularJS (1.2.16) web app with a RESTful API, and I'd like to send 401 Unauthorized responses for requests where authentication information is invalid or not present. When I do so, even with an HTTP interceptor present, I see the browser-presented basic "Authentication Required" dialog when an AJAX request is made via AngularJS. My interceptor runs after that dialog, which is too late to do something useful.

A concrete example:

My backend API returns 401 for /api/things unless an authorization token is present. Nice and simple.

On the AngularJS app side, I've looked at the docs and set up an interceptor like this in the config block:

$httpProvider.interceptors.push(['$q', function ($q) {
  return {
    'responseError': function (rejection) {
      if (rejection.status === 401) {
        console.log('Got a 401')
      }
      return $q.reject(rejection)
    }
  }
}])

When I load my app, remove the authentication token, and perform an AJAX call to /api/things (to hopefully trigger the above interceptor), I see this:

Authentication Required

If I cancel that dialog, I see the console.log output of "Got a 401" that I was hoping to see instead of that dialog:

Got a 401

Clearly, the interceptor is working, but it's intercepting too late!

I see numerous posts on the web regarding authentication with AngularJS in situations just like this, and they all seem to use HTTP interceptors, but none of them mention the basic auth dialog popping up. Some erroneous thoughts I had for its appearance included:

  • Missing Content-Type: application/json header on the response? Nope, it's there.
  • Need to return something other than promise rejection? That code always runs after the dialog, no matter what gets returned.

Am I missing some setup step or using the interceptor incorrectly?

Collin Allen
  • 4,449
  • 3
  • 37
  • 52
  • Why you didn't use 403 instead of 401? – M Rostami Feb 09 '16 at 10:48
  • I used a 401 because, in the original question, I removed the authentication token (in my case, a cookie). 401 is the right response code to use here because the request requires authentication and none was provided. 403 is for when authentication is provided but the server denies access to the resource. – Collin Allen Feb 09 '16 at 16:27

3 Answers3

12

Figured it out!

The trick was to send a WWW-Authenticate response header of some value other than Basic. You can then capture the 401 with a basic $http interceptor, or something even more clever like angular-http-auth.

Collin Allen
  • 4,449
  • 3
  • 37
  • 52
  • 8
    Could you elaborate on that please ? "Something even more clever like angular-http-auth" require that the server send a 200 response. I'm looking for a solution where the server return a 401 code. – Romain Oct 10 '14 at 14:04
  • I think it only popup when 'Authorization': 'Basic XXX' – killebytes Nov 12 '14 at 07:37
  • please post some code example I am having the same problem. IOS basic auth request never finishes – Sumama Waheed Jun 17 '16 at 17:18
  • Is there any example to use this angular-http-auth and intercept the response to modify request header? – NIrav Modi Feb 23 '17 at 11:04
11

I had this issue together with Spring Boot Security (HTTP basic), and since Angular 1.3 you have to set $httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest'; for the popup not to appear.

Collin Allen
  • 4,449
  • 3
  • 37
  • 52
Busata
  • 1,088
  • 1
  • 10
  • 24
  • 2
    it does no t work for my app with Angular 1.5 and firefox 48.x... the basic auth prompt is fired even with X-Requested-With header... – ptitjuju69 Aug 30 '16 at 09:39
  • Same for me.I had added this header into postman. but it's not working. – NIrav Modi Feb 23 '17 at 10:58
  • 1
    this is solved my issue. browsers authentication modal was hidden after applied your issue. Thanks. – luffy Aug 02 '17 at 11:21
  • This idea worked for me, although I created an interceptor and only added this header if the request URL was for my REST API. – remarsh Jan 05 '18 at 22:12
1

For future reference

I've come up with this solution when trying to handle 401 errors. I didn't have the option to rewrite Basic to x-Basic or anything similar, so I've decided to handle it on client side with Angular.

When initiating a logout, first try making a bad request with a fake user to throw away the currently cached credentials.

I have this function doing the requests (it's using jquery's $.ajax with disabled asynch calls):

function authenticateUser(username, hash) {
    var result = false;
    var encoded = btoa(username + ':' + hash);

    $.ajax({
        type: "POST",
        beforeSend: function (request) {
            request.setRequestHeader("Authorization", 'Basic ' + encoded);
        },
        url: "user/current",
        statusCode: {
            401: function () {
                result = false;
            },
            200: function (response) {
                result = response;
            }
        },
        async: false
    });

    return result;
}

So when I try to log a user out, this happens:

//This will send a request with a non-existant user.
//The purpose is to overwrite the cached data with something else
accountServices.authenticateUser('logout','logout');

//Since setting headers.common.Authorization = '' will still send some
//kind of auth data, I've redefined the headers.common object to get
//rid of the Authorization property
$http.defaults.headers.common = {Accept: "application/json, text/plain, */*"};
Alex Szabo
  • 3,274
  • 2
  • 18
  • 30