Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CORS AngularJS Not Sending Cookie/Credentials with POST or DELETE but GET OK

I have an ExpressJS API server and an ExpressJS w/ AngularJS app running in local dev.

The CORS GET requests work fine but the POST and DELETE are not sending any cookies (credentials).

I've tried all sorts of config options but never does the app send credentials for a POST or DELETE.

I hope someone is familiar with this and might see where I'm going wrong.

I've set up two local domains in /etc/hosts:

127.0.0.1 rsm.local
127.0.0.1 api.rsm.local

So http://rsm.local is sending api requests to http://api.rsm.local

AngularJS is using Restangular.

Sessions are stored in MongoDB with .rsm.local for the domain so both servers can read the cookie.

Both client app and api server use the same cookie key and secret.

app.use(express.session({
key: config.cookie_key,
secret: config.cookie_secret,
cookie: {
    domain:'.rsm.local',
    expires: config.cookie_expire
},
store: new mongoStore({
    url: config.database,
    collection: 'sessions',
    auto_reconnect: true
})
}));

This seems to work fine the session state is shared OK between the two applications.

The API server is setup with standard and what I think is correct CORS headers:

app.all('/api/*', function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', 'http://rsm.local:3000');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.setHeader("Access-Control-Allow-Headers", "Accept, Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With");
res.setHeader('Access-Control-Allow-Credentials', true);
next();
});

The client app AngularJS has Restanguarl configured to send credentials correctly I believe:

angular.module('rsm').config(function (RestangularProvider) {

RestangularProvider.setBaseUrl('http://api.rsm.local:3001/api/v1');

RestangularProvider.setDefaultHttpFields({
withCredentials: true,
useXDomain : true
});

});

So everything works fine with GET requests (eg list all and list one):

Heres a sample of a GET request (no pre-flight request), we can see the cookie is sent cross domain and the response works as the CORS all line up correct:

Request URL:http://api.rsm.local:3001/api/v1/articles
Request Method:GET
Status Code:200 OK

Request Headers
GET /api/v1/articles HTTP/1.1
Host: api.rsm.local:3001
Connection: keep-alive
Accept: application/json, text/plain, */*
Origin: http://rsm.local:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Referer: http://rsm.local:3000/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: rsm.sid=s%3A84buSYdgwFPljnBdSdXZhGpe.z86NeNf%2F%2FT9Rn2t9MAaf3%2B4YAnXGsvSbb3nAh0spqZw; XSRF-TOKEN=JtmKfwWOhxxHxtyYR%2B2HdPSfW8e8T7ofUeROE%3D

Response Headers
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Accept, Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With
Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Origin:http://rsm.local:3000
Connection:keep-alive
Content-Length:914
Content-Type:application/json; charset=utf-8
Date:Fri, 06 Dec 2013 15:04:53 GMT

However when doing a POST or DELETE using the same configuration never is the cookie sent.

Here's an example of a pre-flight options request (which is not supposed to send any credentials)

Request URL:http://api.rsm.local:3001/api/v1/articles
Request Method:OPTIONS
Status Code:200 OK

Request Headers
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Access-Control-Request-Headers:accept, content-type
Access-Control-Request-Method:POST
Connection:keep-alive
Host:api.rsm.local:3001
Origin:http://rsm.local:3000
Referer:http://rsm.local:3000/
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)    Chrome/31.0.1650.63 Safari/537.36

Response Headers
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Accept, Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With
Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Origin:http://rsm.local:3000
Allow:GET,POST,PUT,HEAD,DELETE,TRACE,COPY,LOCK,MKCOL,MOVE,PROPFIND,PROPPATCH,UNLOCK,REPORT,MKACTIVITY,CHECKOUT,MERGE,M-SEARCH,NOTIFY,SUBSCRIBE,UNSUBSCRIBE,PATCH
Connection:keep-alive
Content-Length:154
Content-Type:text/html; charset=utf-8
Date:Fri, 06 Dec 2013 15:04:23 GMT

So this options request seems to say "Yes accept POST and the Origin URLS match up"

But the subsequest POST request lacks the Cookie

Request URL:http://api.rsm.local:3001/api/v1/articles

Request Headers
POST http://api.rsm.local:3001/api/v1/articles HTTP/1.1
Accept: application/json, text/plain, */*
Referer: http://rsm.local:3000/
Origin: http://rsm.local:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)    Chrome/31.0.1650.63 Safari/537.36
Content-Type: application/json;charset=UTF-8
Request Payload
...
...
...

There are many threads here and elsewhere on the web that all seem to suggest using the configurations I have but still no credentials sent with the POST/DELETE.

I'm pretty much stuck now, hoping another set of eyes can see something?

Why would GET be OK and POST not?

I'm on a Ubuntu workstation using Chrome and FireFox - both don't send credentials.

Thanks.

like image 961
Rudi Starcevic Avatar asked Nov 12 '22 17:11

Rudi Starcevic


1 Answers

I found a solution that works OK for me.

I'm using a combo of ExpressJS csrf(), passortjs and CORS headers.

The csrf() was failing which borked everything else and resulted in the browser showing headers without credentials in the console.

The solution was to add one more request type lookup in connect's defaultValue function.

I've created a new one and use it like this:

function csrfValue(req) {
        return (req.body && req.body._csrf)
            || (req.query && req.query._csrf)
            || (req.headers['x-csrf-token'])
            || (req.headers['x-xsrf-token'])
            || (req.cookies['XSRF-TOKEN']);
    }

app.use(express.csrf({value: csrfValue}));

This makes everything work fine.

I also found that using Restangular I was able to set a custom header for x-xsrf-token but only Chrome would set this - FireFox would not.

So I settle on the csrfValue() solution.

Cheers.

like image 189
Rudi Starcevic Avatar answered Nov 15 '22 12:11

Rudi Starcevic