Here's my setup:
- I have an http server implemented in nodejs that exposes api endpoints. This is reverse proxied through nginx to api.domain.com
with ssl. Here's the config:
1 server {
2 listen 80;
3 server_name api.domain.com;
4 access_log /var/log/nginx/api.access.log;
5 location / {
6 proxy_pass http://127.0.0.1:3000/;
7 }
8 }
9
10 server {
11 listen 443;
12 server_name api.domain.com;
13 access_log /var/log/nginx/api.access.log;
14 ssl on;
15 ssl_certificate /path/to/ssl/server.crt;
16 ssl_certificate_key /path/to/ssl/server.key;
17 location / {
18 proxy_pass https://127.0.0.1:3001/;
19 }
20 }
Then I have nginx delivering a static context file under dashboard.domain.com
that is intended to consume the api from api.domain.com
. Here is the setup:
1 server {
2 listen 80;
3 server_name dashboard.domain.com;
4 root /path/to/static/site;
5 }
I want to do this using CORS, I made sure the js on the static site is sending the correct Origin
header in all requests. I implemented a very simple login mechanism. Here's the coffeescript code I'm using on the api endpoint:
# server.coffee
app.configure ->
app.use middleware.setP3PHeader()
app.use express.bodyParser()
app.use express.cookieParser()
app.use express.session
secret: conf.session.secret
key: conf.session.key
cookie:
maxAge: conf.session.maxAge
app.use express.methodOverride()
app.use express.query()
app.use express.errorHandler()
# routes.coffee
app.options '*', shop.cors, shop.options
app.post '/login', shop.cors, shop.login
app.post '/logout', shop.cors, shop.logout
app.get '/current-user', shop.cors, shop.current
# shop.coffee
exports.options = (req, res) ->
res.send 200
exports.cors = (req, res, next) ->
allowed = ['http://dashboard.domain.com', 'http://localhost:3000']
origin = req.get 'Origin'
if origin? and origin in allowed
res.set 'Access-Control-Allow-Origin', origin
res.set 'Access-Control-Allow-Credentials', true
res.set 'Access-Control-Allow-Methods', 'GET,POST'
res.set 'Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'
next()
else
res.send 403, "Not allowed for #{origin}"
exports.login = (req, res) ->
unless req.body.email? and req.body.password?
res.send 400, "Request params not correct #{req.body}"
models.Shop.findOne()
.where('email').equals(req.body.email)
.where('password').equals(req.body.password)
.exec (err, shop) ->
if err? then return res.send 500, err.message
unless shop? then return res.send 401, "Not found for #{req.body}"
req.session.shopId = shop.id
res.send 200, shop.publish()
exports.logout = (req, res) ->
delete req.session.shopId
res.send 200
exports.current = (req, res) ->
unless req.session.shopId?
return res.send 401, "Not logged in!"
models.Shop.findById(req.session.shopId)
.exec (err, shop) ->
if err? then return send.res 500, err.message
unless shop? then return res.send 404, "No shop for #{req.session.shopId}"
res.send 200, shop.publish()
The problem is this:
1. I first make a call to /login
and I get a new session with a logged in user (req.session.shopId
)
2. Then I call /current-user
but the session is gone! The session id received by the nodejs server is different and thus it creates a different session
It looks like you have a global proxy directive (proxy_pass
), but you're not explicitly handling header forwarding (where, presumably the session token lives as a cookie), and further you need to think about what constitutes a (shared) session cache key.
Might try something like this:
location / {
proxy_pass http://127.0.0.1:3000/;
proxy_redirect off;
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;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_cache pcache;
proxy_cache_key "$scheme$host$request_method$request_uri";
}
Also, if the node.js server is on the same box, not sure why you would connect via ssl on localhost (proxy_pass https://127.0.0.1:3001/
). You might want to consider exposing an SSL-only public face with a rewrite directive by doing a: rewrite ^ https://api.domain.com$request_uri? permanent;
See also: node.js + nginx - And now? [SO] on basic setup, and http://www.ruby-forum.com/topic/4408747 for a good discussion on lost (and worse - leaked!) sessions from a nginx reverse proxy configuration snafu.
If you want to have session persistence, you need to be sure that you configured express-session
module correctly. resave
and saveUninitialized
parameters are important.
From docs:
resave
Forces the session to be saved back to the session store, even if the session was never modified during the request. Depending on your store this may be necessary, but it can also create race conditions where a client makes two parallel requests to your server and changes made to the session in one request may get overwritten when the other request ends, even if it made no changes (this behavior also depends on what store you're using).
The default value is true, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case. Typically, you'll want false.
How do I know if this is necessary for my store? The best way to know is to check with your store if it implements the touch method. If it does, then you can safely set resave: false. If it does not implement the touch method and your store sets an expiration date on stored sessions, then you likely need resave: true.
saveUninitialized
Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with race conditions where a client makes multiple parallel requests without a session.
The default value is true, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case.
Note if you are using Session in conjunction with PassportJS, Passport will add an empty Passport object to the session for use after a user is authenticated, which will be treated as a modification to the session, causing it to be saved.
One of my applications has this and it's working:
app.use(session({
store: new RedisStore({
host: config.redis_instance_local_ip,
port: config.redis_instance_local_port
}),
secret: config.application_session_secret,
resave: false,
saveUninitialized: true
}));
Additionally, if you want to have sticky sessions (or session persistence) over multiple nodes load balanced by nginx, you should have commercial version of nginx, nginx plus. See http://nginx.com/products/session-persistence/