Session ID is regenerated when CORS behind Nginx

2,539 views
Skip to first unread message

Renaud Kyokushin

unread,
Jun 17, 2014, 2:32:59 AM6/17/14
to expre...@googlegroups.com
Hi,

I was previously using Apache + PHP (Phalcon) + Memcached, and I told myself "why not using NodeJS ?". And now I have Nginx (as a proxy), NodeJS (Express 4), and Redis.

My Nginx proxy is serving several NodeJS servers, one per subdomain (each subdomain listens localhost on a different port). Everything works fine, except for sessions.

When I perform an Ajax CORS request from http://myapp.local to http://api.myapp.local/login , Express receives the proper Cookie header value (session id), but then my client receives a response with a Set-Cookie header containing a newly generated session id.

I don't think Nginx is allowing itself to modify the session id inside that cookie, so it must be a configuration issue with Express.

However, I thought I did everything by the book:
- Correctly configured Nginx for CORS (handling OTIONS requests, allowing only one subdomain per request, allowing credentials, ...)
- Sent credentials with my Ajax POST request (using jQuery xhrFields : withCredentials=true)
- Express-side: simply configure the session module (domain = '.myapp.local', path = '/'), and use app.enable('trust proxy');

Again, I receive the proper cookie with the proper value, but why does Express send back a new session id? It is messing with my session...

I can provide any code necessary.

Also, I was using a "Authorization" header back when I was using PHP for this app, and a pool of authorized tokens stored in Memcached (and a cookie set by the client to remember this token), so maybe I should still use this? This would erase the cookies and CORS related issues immediately. Is this easy/preferable, in NodeJS?

Thank you all for your help!

Best regards,

Alex Yaroshevich

unread,
Jun 17, 2014, 12:04:03 PM6/17/14
to expre...@googlegroups.com
Any modern auth based on cookies. Cookies are just headers. 'Set-Cookie' header in response, and 'Cookie' header in request.

You can easily emulate browser request and check the response to verify business logic of your app.
For example like that: curl -v 'http://api.myapp.local/login' -H 'Cookie: ...' etc.

Nginx can use its own session module. Please check it's disabled.

Check that requests to nginx and to your node app have the same responses. Just send data to nginx port, and then to node app port and compare results.

It's common rules that can help you just because I don't have your sources.

[vanga mode on]
Probably, some headers was not provided correctly via nginx, or you have a business logic in your express app: look at request headers and at cookie-header parser.
[/vanga mode off]

Alex Yaroshevich

unread,
Jun 17, 2014, 12:09:53 PM6/17/14
to expre...@googlegroups.com
btw. Probably you just need to set 'Access-Control-Allow-Origin: *' headers in nginx. Without it browsers can deny Cookie headers. So they will be set in response but browsers will ignore them.


On Tuesday, June 17, 2014 10:32:59 AM UTC+4, Renaud Kyokushin wrote:

Renaud Kyokushin

unread,
Jun 17, 2014, 3:22:13 PM6/17/14
to expre...@googlegroups.com
Hi Alex,

Thanks for this information.

Regarding the allow-origin = "*" , I can't do it because otherwise it would refuse my CORS request, saying that it is not secure to use "*" or something.

I will try to give you some code, if you have the time of course. I think that I will go back to using only a "Authorization" header and no cookie, if I can't get it to work. It is frustrating to lose so much time on something so "trivial".


Please find below my nginx.conf for the api.myapp.local server:

server {
    listen      80;
    server_name api.myapp.local;
    error_log   logs/api.error.log notice;
    access_log  logs/api.access.log;
    location / {
        if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) {
            return 405;
        }
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, HEAD, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Referer,Accept,Origin,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,X-HTTP-Method-Override,If-Modified-Since,Cache-Control,Content-Type,Cookie';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, HEAD, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Referer,Accept,Origin,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,X-HTTP-Method-Override,If-Modified-Since,Cache-Control,Content-Type,Cookie';
            add_header 'Access-Control-Max-Age' 1728000;
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, HEAD, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Referer,Accept,Origin,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,X-HTTP-Method-Override,If-Modified-Since,Cache-Control,Content-Type,Cookie';
            add_header 'Access-Control-Max-Age' 1728000;
        }
        proxy_pass http://localhost:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto  $scheme;
    }
}


Find below my NodeJS server for api.myapp.local:

var app = express();
app.enable('trust proxy');

// ... ... ...
 
require('../common/webapp_session.js')(app, conf);

var iPort = process.env.PORT || conf.port;
app.listen(iPort);
console.log('API on port ' + iPort);


Here is webapp_session.js:

app.use(cookieParser());
console.log('** COOKIE SESSION');
console.log(conf.cookie);
app.use(session({
    secret: conf.cookie.secret,
    name: conf.cookie.name,
    cookie: {secure: conf.cookie.secure, domain: conf.cookie.domain, maxAge: 1000*60*60*24}
}));


The Express configuration values for session:

{ cookie:
   { path: '/',
     _expires: Mon Jun 16 2014 23:24:06 GMT+0200 (Paris, Madrid (heure d’été)),
     originalMaxAge: 86400000,
     httpOnly: true,
     secure: false,
     domain: '.myapp.local' } }


And last, my ajax call (I have the same problem if I just GET a web page on api.myapp.local, instead of doing a CORS POST) :

$.ajax('http://api.myapp.local/login',
{
    type: 'POST',
    data: oForm.serialize(),
    crossDomain: true,
    xhrFields: {
        withCredentials: true
    },
    success: function(res)
    {
            // ...
    }
});


The problem is that Express seems to regenerate a new session ID after each request on another subdomain (CORS or not).

Is it due to the fact that the Express servers are listening to localhost, and not to the specific domains I am using in cookies?

Thank you for any help,

Alex Yaroshevich

unread,
Jun 17, 2014, 3:31:58 PM6/17/14
to expre...@googlegroups.com
Yeah. Can you check your $http_origin?
Also can you try to look at Network tab in Chrome DevTools / Firebug?
Look at browser request sent to server. What's in the Cookie header?
What's in the response Set-Cookie and Access-Control-* headers?

I don't think the problem in local domain name but you also can try to emulate some more real like api.myapp.local.com.

Renaud Kyokushin пишет:
--
You received this message because you are subscribed to a topic in the Google Groups "Express" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/express-js/8f2OiL5IgrA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to express-js+...@googlegroups.com.
To post to this group, send email to expre...@googlegroups.com.
Visit this group at http://groups.google.com/group/express-js.
For more options, visit https://groups.google.com/d/optout.

Renaud Kyokushin

unread,
Jun 17, 2014, 4:55:18 PM6/17/14
to expre...@googlegroups.com
Here is the detail of my call to http://api.myapp.local/login, as you can see everything seems fine, except for the Set-Cookie in the response headers, which has changed after the request.

Remote Address:127.0.0.1:80
Request URL:http://api.myapp.local/login
Request Method:POST
Status Code:200 OK
 
== Request Headers ==
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
Connection:keep-alive
Content-Length:34
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
Cookie:auth-token=s%3AzQIKK9ORZDxMALCUEdbAqVzO.mCcpuR4EFX22vUV7XSsVFZlMJy0HjPWzBFEDWGUqCzs
Host:api.myapp.local
Origin:http://myapp.local
Referer:http://myapp.local/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
 
== Form Data == URL encoded
email:de...@myapp.io
password:demo
 
== Response Headers ==
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Referer,Accept,Origin,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,X-HTTP-Method-Override,If-Modified-Since,Cache-Control,Content-Type,Cookie

Access-Control-Allow-Methods:GET, POST, HEAD, OPTIONS
Access-Control-Allow-Origin:http://myapp.local
Connection:keep-alive
Content-Length:16
Content-Type:application/json; charset=utf-8
Date:Tue, 17 Jun 2014 20:50:58 GMT
Server:nginx/1.7.1
Set-Cookie:auth-token=s%3A6etReaFh1Z6A8F2WnX4PBG09.vyN3YjodDafKMnM2YNtcJt37t9Jiw553I4ZaInpKnV4; Domain=.myapp.local; Path=/; Expires=Wed, 18 Jun 2014 20:50:58 GMT; HttpOnly
X-Powered-By:Express

Renaud Kyokushin

unread,
Jun 17, 2014, 5:02:12 PM6/17/14
to expre...@googlegroups.com
I just tested right now, and the same bug happens if I try to make a request from http://client1.myapp.local (to http://api.myapp.local/login), therefore this is not a "no subdomain" related issue (for "http://myapp.local")

In fact, there should not be any "Set-Cookie" directive in the response headers. Why is Express generating a new session id whereas it is receiving a valid one?

Alex Yaroshevich

unread,
Jun 17, 2014, 5:10:09 PM6/17/14
to expre...@googlegroups.com
You should manually check `auth-token` cookie.
Probably there are some misunderstood or even a bug.
Try to check Cookie value on the express side about CORS auth middleware.
Probably you need to tell nginx to pass Cookie manually (it will be strange if it is):
        proxy_set_header Cookie $http_cookie;


p.s. Maybe CORS middleware uses host for salt?

Renaud Kyokushin пишет:

Renaud Kyokushin

unread,
Jun 17, 2014, 5:20:50 PM6/17/14
to expre...@googlegroups.com
Whereas the middleware uses host for salt is clearly beyond my competence :)

I already verified the "auth-token" value before asking for help. I did a lot of testing and configurations modifications :(

The cookie is well set, as you can see below:


Debug session before login:
{ cookie:
   { path: '/',
     _expires: Wed Jun 18 2014 22:56:46 GMT+0200 (Paris, Madrid (heure d’été)),
     originalMaxAge: 86400000,
     httpOnly: true,
     secure: false,
     domain: '.skol.local' } }

Debug request headers during login:
{ connection: 'upgrade',
  host: 'api.myapp.local',
  'x-nginx-proxy': 'true',
  'x-real-ip': '127.0.0.1',
  'x-forwarded-for': '127.0.0.1',
  'x-forwarded-proto': 'http',
  'content-length': '34',
  accept: '*/*',
  origin: 'http://client1.myapp.local',
  'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/
537.36',
  'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  'accept-encoding': 'gzip,deflate,sdch',
  'accept-language': 'fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4',
  cookie: 'auth-token=s%3A5ZWkEQ246Xv33VLFUjUHpFqB.CyHaw9QvksI%2BGbssum%2Fx%2F9UgVuUA4pi%2FaDwlx3vaMz4' }

Debug session after login:
{ cookie:
   { path: '/',
     _expires: Wed Jun 18 2014 22:56:46 GMT+0200 (Paris, Madrid (heure d’été)),
     originalMaxAge: 86400000,
     httpOnly: true,
     secure: false,
     domain: '.skol.local' },
  user: 'blabla' }


You see? Everything seems fine, that is why it is so disturbing :-/
email...@myapp.io
password:demo
== Response Headers ==
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Referer,Accept,Origin,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,X-HTTP-Method-Override,If-Modified-Since,Cache-Control,Content-Type,Cookie
Access-Control-Allow-Methods:GET, POST, HEAD, OPTIONS
Access-Control-Allow-Origin:http://myapp.local
Connection:keep-alive
Content-Length:16
Content-Type:application/json; charset=utf-8
Date:Tue, 17 Jun 2014 20:50:58 GMT
Server:nginx/1.7.1
Set-Cookie:auth-token=s%3A6etReaFh1Z6A8F2WnX4PBG09.vyN3YjodDafKMnM2YNtcJt37t9Jiw553I4ZaInpKnV4; Domain=.myapp.local; Path=/; Expires=Wed, 18 Jun 2014 20:50:58 GMT; HttpOnly
X-Powered-By:Express

Renaud Kyokushin

unread,
Jun 17, 2014, 5:23:18 PM6/17/14
to expre...@googlegroups.com
(domain in cookie debug is .myapp.local, the .skol.local is a replacement I forgot to make when writing this email to hide my project name on localhost because it is irrelevant)

Alex Yaroshevich

unread,
Jun 17, 2014, 5:46:41 PM6/17/14
to expre...@googlegroups.com
Okay. I'm really disoriented.
What is cookieParser? Can you try to disable it?

Also try to debug your cookie/session parser:

app.use(function (req, res, next) {
    console.log(req.headers);
    next();
});

app.use(cookieParser());
app.use(function (req, res, next) {
    console.log(req.headers, req.cookie);
    next();
});

app.use(session({
    secret: conf.cookie.secret,
    name: conf.cookie.name,
    cookie: {secure: conf.cookie.secure, domain: conf.cookie.domain, maxAge: 1000*60*60*24}
}));
app.use(function (req, res, next) {
    // ... dump cookie, session
    next();
});


Renaud Kyokushin пишет:

Renaud Kyokushin

unread,
Jun 18, 2014, 2:28:18 AM6/18/14
to expre...@googlegroups.com
Hi,

cookieParser is a mandatory Express module when it comes to cookies, you have to call it BEFORE handling session, otherwise it won't work.

I have an interesting info: I displayed "req.sessionID" in my login() function, and it was not the value I was expecting! As if the Cookie header was not passed correctly to Express... But in fact, it is!!!

login: function(req, res)
{
console.log('**** login');
console.log(req.sessionID); // ==> Qb2il8Mx0ogl5UX98hV7TgQG
console.log(req.session);
console.log(req.headers); // ==> { ... , 'cookie': 'auth-token=s%3Axt8FPBAcWKbIwf2tovdy7VY6.XQ5VXXAYbST3IGdGPV4PuqLSm4bDXJndJMJB0jxn82c'}
//console.log(req.body);
req.session.user = 'blabla';
req.session.save();
console.log(req.session);
res.json({success: true});
},

And the Set-Cookie header I receive in response to this request is:
Set-Cookie:auth-token=s%3AQb2il8Mx0ogl5UX98hV7TgQG.9exoE9EPshf06%2FoD5VMClpk7mZsg0mPFTXz%2F77wZi9E; Domain=.myapp.local; Path=/; Expires=Thu, 19 Jun 2014 06:19:47 GMT; HttpOnly


So, in order to sum up:

1) It is confirmed that Express gets the proper "cookie" header, with the proper auth-token value
2) But Express still generates a new session ID
3) And then Express sends a Set-Cookie header with this new session ID

... Why ?

Renaud Kyokushin

unread,
Jun 18, 2014, 3:12:18 AM6/18/14
to expre...@googlegroups.com
Ok nevermind...

I think I will go back to the system I was using with PHP:

Thank you for you help anyway, Alex, much appreciated!
Reply all
Reply to author
Forward
0 new messages