29

I'm using AngularJS v1.2.4.

I had an issue with Angular sending a preflight OPTIONS call (Chrome was showing the OPTIONS call as 'canceled') and resolved it with:

$httpProvider.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];

That worked for all my $resource calls, and everything was good.

Now I'm trying to implementation authentication, and a login page that sends a POST request to my server with the user's credentials. I'm seeing the problem I was facing before, but $resource calls are still working fine.

What's really frustrating is that the problem happens intermittently; I'll change a few options surrounding the headers, then it'll work for a bit, and stop working again without any code change.

My server is configured for CORS and works fine with curl, and other REST clients. Here's an example:

curl -X OPTIONS -ik 'https://localhost:3001/authenticate' -H "Origin: https://localhost:8001"
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 2
cache-control: no-cache
access-control-allow-origin: *
access-control-max-age: 86400
access-control-allow-methods: GET, HEAD, POST, PUT, DELETE, OPTIONS
access-control-allow-headers: Authorization, Content-Type, If-None-Match, Access-Control-Allow-Headers, Content-Type
access-control-expose-headers: WWW-Authenticate, Server-Authorization
set-cookie: session=Fe26.2**94705d49717d1273197ae86ce6661775627d7c6066547b757118c90c056e393b*2KYqhATojPoQhpB2OwhDwg*W9GsJjK-F-UPqIIHTBHHZx1RXipo0zvr97_LtTLMscRkKqLqr8H6WiGd2kczVwL5M25FBlB1su0JZllq2QB-9w**5510263d744a9d5dc879a89b314f6379d17a39610d70017d60acef01fa63ec10*pkC9zEOJTY_skGhb4corYRGkUNGJUr8m5O1US2YhaRE; Secure; Path=/
Date: Wed, 18 Dec 2013 23:35:56 GMT
Connection: keep-alive

Here's the $http.post call:

var authRequest = $http.post('https://' + $location.host() + ':3001/authenticate', {email: email, password: password});

When the call from my app works, this is how the OPTIONS request looks like:

enter image description here

When it doesn't work, this is the OPTIONS request:

enter image description here

It looks like a whole bunch of header attributes are missing. Has anyone encountered a similar issue?

Edit:

Just to clarify, when it doesn't work, the request never makes it to the server - it's instantly aborted in the browser.

enter image description here

In Firebug, the request headers are:

OPTIONS /authenticate HTTP/1.1
Host: localhost:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:25.0) Gecko/20100101 Firefox/25.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.91,en-GB;q=0.82,fr-FR;q=0.73,fr;q=0.64,utf-8;q=0.55,utf;q=0.45,de-DE;q=0.36,de;q=0.27,en-sg;q=0.18,en-ca;q=0.09
Accept-Encoding: gzip, deflate
Origin: https://localhost:8001
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Proxy-Authorization: Basic cGF0cmljZUB6b25nLmNvbTpjaGFuZ2VtZQ==
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

enter image description here

Update:

I've eliminated the possibly of a problem with the server, I think, by changing the host to a non-existent server. Still seeing the same behavior.

Here's some code:

App.services.factory('AuthService', function ($http, $location, $q) {

    var currentUser;

    return {
        authenticate: function (email, password) {

            //promise to return
            var deferred = $q.defer();

            var authRequest = $http.post('https://this.does.not.exist.com:3001/authenticate', {email: email, password: password});

            authRequest.success(function (data, status, header, config) {
                currentUser = data;
                console.log('currentUser in service set to:');
                console.log(currentUser);
                //resolve promise
                deferred.resolve();
            });

            authRequest.error(function (data, status, header, config) {
                console.log('authentication error');
                console.log(status);
                console.log(data);
                console.log(header);
                console.log(config);

                //reject promise
                deferred.reject('authentication failed..');
            });

            return deferred.promise;
        },
        isAuthenticated: function () {
            return currentUser !== undefined;
        }
    };
});

and HTTP Config:

App.config(['$httpProvider', function ($httpProvider) {

    $httpProvider.defaults.useXDomain = true;
    //$httpProvider.defaults.headers.common = {};

    console.log('logging out headers');
    console.log($httpProvider.defaults);
    console.log($httpProvider.defaults.headers.common);
    console.log($httpProvider.defaults.headers.post);
    console.log($httpProvider.defaults.headers.put);
    console.log($httpProvider.defaults.headers.patch);
    console.log('end logging out headers');

    $httpProvider.defaults.headers.common = {Accept: "application/json, text/plain, */*"};
    $httpProvider.defaults.headers.post = {"Content-Type": "application/json;charset=utf-8"};

    console.log('after: logging out headers');
    console.log($httpProvider.defaults.headers.common);
    console.log($httpProvider.defaults.headers.post);
    console.log($httpProvider.defaults.headers.put);
    console.log($httpProvider.defaults.headers.patch);
    console.log('after: end logging out headers');

    $httpProvider.interceptors.push(function ($location, $injector) {
        return {
            'request': function (config) {

                console.log('in request interceptor!');

                var path = $location.path();
                console.log('request: ' + path);

                //injected manually to get around circular dependency problem.
                var AuthService = $injector.get('AuthService');
                console.log(AuthService);
                console.log(config);

                if (!AuthService.isAuthenticated() && $location.path() != '/login') {
                    console.log('user is not logged in.');
                    $location.path('/login');
                }

                //add headers
                console.log(config.headers);
                return config;
            }
        };
    });
}]);
shaunlim
  • 4,384
  • 6
  • 33
  • 38
  • 1
    are you sure your script/app is not sending double request? sync (form submit) + async request to the same url? – asumaran Dec 19 '13 at 07:30
  • @asumaran can you clarify please? i'm pretty sure it's only a single request though. – shaunlim Dec 19 '13 at 09:26
  • Use firebug for debugging CORS problems. Chrome has a known issue and will not display all of the information available. https://code.google.com/p/chromium/issues/detail?id=269192&q=CORS&colspec=ID%20Pri%20M%20Iteration%20ReleaseBlock%20Cr%20Status%20Owner%20Summary%20OS%20Modified – TheSharpieOne Dec 19 '13 at 14:54
  • @TheSharpieOne same thing in Firebug. I've edited my question to reflect that. – shaunlim Dec 19 '13 at 17:51

3 Answers3

13

This feels like it might be related to the fact that you're hitting an https endpoint on your localhost. That means you're probably using some sort of self-signed SSL certificate, which may mean Chrome considers it untrusted.

I'd first try going directly to the /authenticate endpoint and see if Chrome gives you a warning about an untrusted certificate. See if accepting that warning works.

Otherwise, possibly while you're testing locally you can hit just an http endpoint and see if that solves things?

Michael Cox
  • 1,281
  • 13
  • 15
  • Hey Michael, thanks for the response! The HTTPS issue might be a possibility, but I'm also accessing HTTPS endpoints (on the same server) using the $resource object in other parts of my app and everything seems okay there. I'll give your suggestions a go tomorrow morning anyway. – shaunlim Dec 23 '13 at 07:38
  • I visited the '/authenticate' endpoint in Chrome, and there was no warning about an untrusted certificate; I probably accepted it at some point before this when I first start developing the app. – shaunlim Dec 23 '13 at 17:57
  • Also, it seems to work well when I change to call to $http.get, and have a corresponding GET /authenticate endpoint on the server. – shaunlim Dec 23 '13 at 23:49
  • I observed same behaviors with my $resources. Since you send a POST or PUT request from localhost (at least), Chrome start with an OPTIONS request. I do not know why (probably to perform some security controls before) but that is the way it works. – M'sieur Toph' Jan 10 '15 at 21:49
9

A huge thank you to Michael Cox for pointing me in the right direction. I accept his answer since it led me to the solution, but here are more details:

Looking into the https issue, I found:

My problem was slightly different though. It still wasn't working after I followed the instructions in the links above. I read the chrome "untrusted" message carefully and it was something like "you're trying to access mylocalhost.com but the server is representing itself as ".

It turns out that my hastily created self signed certificate was "server.crt" when it should be "mylocalhost.crt"

Community
  • 1
  • 1
shaunlim
  • 4,384
  • 6
  • 33
  • 38
7

You can not have allow-credentials together with Access-Control-Allow-Origin: *.

Important note: when responding to a credentialed request, server must specify a domain, and cannot use wild carding.

Regarding GET requests, they do not need preflight etc if they don't have special headers.

Source: Mozilla Developer Network. (Best CORS link on the web!)

Zut
  • 639
  • 1
  • 5
  • 12