Using Express and Node, how to maintain a Session across subdomains/hostheaders
Asked Answered
D

3

18

I have a single node server that responds to requests and redirects a user based on host headers. The usage is that the static/home site lives at www and each user has their own sub domain (i.e. www.example.com and site.example.com). The routing is as per site.js.

When the user is not logged in they are redirected to login.

I am discovering that the session is not maintained when the user is redirected to their sub domain. I guess this is expected, but I am wondering if there is a way to maintain the same session across both sub domains.

I was hoping that if they were logged in and returned to www.example.com they would see a different view that included a link to logout / their dashboard, etc. My workaround at the moment, I'm thinking, is to just create the session on their subdomain and if they do return to www it will just be as if they are not logged in.

Anyone dealt with this before or have answers on how to handle sessions in this manner?

I think the issue may be in users.js where I redirect to 'http://site.example.com' as its not a relative path...

Here is the relevant code (the user lookup is done using MongoDB and I've left it out as its working fine - the line that calls this service is users.authenticate)...

server.js:

app.configure ->
app.set "views", "#{__dirname}/views"
app.set "view engine", "jade"
app.use express.bodyParser()
app.use express.methodOverride()
app.use express.cookieParser()
app.use express.session { 
    key: "KEY", 
    secret: "SECRET", 
    store: new MemoryStore(), 
    cookie: { 
        domain: 'example.com', 
        maxAge   : 1000*60*60*24*30*12 
    }
}
app.use express.static "#{__dirname}/public"
app.use express.logger "short"
app.use express.favicon "#{__dirname}/public/img/favicon.ico"
app.use app.router

site.js:

module.exports = (app) ->
app.get '/', (req, res) ->
    console.log "/ hit with #{req.url} from #{req.headers.host}"
    domains = req.headers.host.split "."
    org = if domains then domains[0] else "www"
    if org == "www"
        res.render "index", { layout: null }
    else
        if req.session.user
            console.log "session established"
            res.render "app", { layout: null }
        else
            console.log "no session"
            res.redirect "http://www.example.com/accounts/login"    

users.js:

users = require('../services/users')
module.exports = (app) ->
app.get "/accounts/login", (req, res) ->
    res.render "login", { layout: null, locals: { returnUrl: req.query.returnUrl } }
app.post "/accounts", (req, res) ->
    users.authenticate app, req.body.login, req.body.password, (user) ->
        if user
            req.session.user = user
            res.redirect "http://#{user.orgName}.example.com"
        else
            res.render "login", { layout: null, locals: { returnUrl: req.body.url } }
app.get "/accounts/logout", (req, res) ->
    console.log "deleting user from session"
    delete req.session.user
    res.redirect "http://www.example.com                

To test it locally on OSX, I have added www.example.com and site.example.com in to my hosts file so that the DNS lookups get handled locally.

Diacaustic answered 30/1, 2012 at 22:45 Comment(0)
A
33

First of all to allow browser to make cross-domain requests you need to set headers on server side. This solution works for normal request as well as AJAX. In your express configure function:

Express 4.0:

var express = require('express');
var session = require('express-session');
var cookieParser = require('cookie-parser');

var app = express();

app.use(cookieParser());
app.use(session({
    secret: 'yoursecret',
    cookie: {
        path: '/',
        domain: 'yourdomain.com',
        maxAge: 1000 * 60 * 24 // 24 hours
    }
}));
app.use(function(req, res, next) {
    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Origin', req.headers.origin);
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
    res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
    next();
});

Access-Control-Allow-Origin can be set to '*' if no cross-domain cookies exchange for sessions needed. To have cookies and session shared cross-domain you need to set specific Access-Control-Allow-Origin to actually domain where request is made from, that's why req.headers.origin - is perfect for that.

Using domain it wont work well on localhost - so make sure you disable it in development environment, and enable on production. It will enable shared cookies across top and sub domains.

This is not all. Browsers it self won't send cookies over cross domain requests, and this have to be forced. In jQuery you can add extra parameter in $.ajax() request:

xhrFields: { withCredentials: true }

For non jQuery, just have XHR constructor and set this parameter:

xhr.withCredentials = true;

And you are ready to do cross-domain with shared session.

Altorilievo answered 31/1, 2013 at 13:56 Comment(7)
user $.ajaxSetup to add withCredentials: true to every requestGustatory
@Maksims "Using domain it wont work well on localhost" - why doesn't it work on localhost? I have: api.localhost:3000 and localhost:3000 - not working? :\Carniola
Because by security of browser, cookie will not be sent if current domain does not match its defined domain. For development and stage enviroments you could just comment out domain property, or set it to "localhost".Altorilievo
just in case, if you are using a plain XMLHttpRequest instead of $.ajax() call, then, the withCredentials param can be set using: var request = new XMLHttpRequest() request.withCredentials = trueMalherbe
@Malherbe it is in answer at the bottom, exactly the thing you've wrote ;)Altorilievo
oops. how did I miss that part. my bad.Malherbe
how to do it for Fetch API ? It seems that it does not set any cookies when I set credentials: 'include'. Any ideas ?Rincon
N
2

Did you make sure you have your cookies set to the top-level domain so it can be read by all subdomains? Then it's just a matter or persisting your session data in memory, a db, whatever as usual. I don't have my dev machine up and running, but it'll be something like this in your app.configure().

app.use(express.cookieParser());

app.use(express.session({  
  key: 'A_SESSION_KEY',   
  secret: 'SOMETHING_REALLY_HARD_TO_GUESS',   
  store: new express.session.MemoryStore,  
  cookie: {  
    path     : '/',  
    domain   : 'yourdomain.com',  
    httpOnly : true,  
    maxAge   : 1000*60*60*24*30*12    //one year(ish)  
  }   
}));
Noisette answered 31/1, 2012 at 1:42 Comment(4)
I've added in your suggestion but it hasn't made any difference. I tried 'example.com', '*.example.com', and '.example.com'. I've added more code in my original question to detail the process a bit better. I think it may be an issue with the redirect...Diacaustic
@Diacaustic Try to remove all cookies in your browser after change your code. This answer seems good to me.Cystotomy
@InspiredJW have done that.. maybe its because I'm running locally? when I set session.user = user and then log the session out to console I can see it in there, but the moment I redirect the user disappears.. It may in fact be unrelated to the sub domain 'issue' I am trying to solve as it appears that my session is simply not working across any routes (even on the same host header).Diacaustic
@Diacaustic have you verified that the session data is being persisted in new MemoryStore()? I'm not sure what your MemoryStore() object makes available for monitoring, but I'd try switching it to a Redis or Memcache store so I could see the keys being added if that's not offered by your store.Noisette
I
1

Note: If using Express 4 and the new cookie-session module, the code looks like

{ 
  secret: <session_secret> , 
  store: <session store> , 
  domain: '.domain.com',
}

This bit me, but the API has changed.

Imprest answered 7/5, 2014 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.