4

For security reasons we want to be able to invalidate all of a user's active sessions, for example if they change their password, or just want to be able to force log out their other sessions. We're using Node.js, Express, express-sessions and the Redis session store. In our app we have (CoffeeScript):

app.use express.session
    cookie:
        maxAge: 5 * 24 * 60 * 60 * 1000 # 5 days in ms
    store: new RedisStore(client: rclient)
    key: "secret-key"

The Redis store works by mapping the unique session id to whatever data you store in the session. For example:

# In an HTTP request
req.session.user = { _id: "user-id" }

in Redis becomes:

> get "sess:<session-id>"
'{ "user": { "_id": "user-id" } } '

What we need is a way to track all sessions that correspond to each user id, so that we can remove these from Redis if we want to invalidate a user's sessions. The following caveats apply:

  1. The sessions are given a TTL in Redis equal to the maxAge of the cookie. The tracking mechanism for each session should also expire after this time to avoid stale data.
  2. Not all sessions will necessarily be associate to a user. Some are just used to track anonymous session details.

The naive approach of adding another reverse look up key in Redis (for example mapping user_id to a set of session ids for the user) fails when it comes to caveat (1).

This feels like a problem that other sites using Express must have encountered, since it's a very common security pattern. Does anyone have any suggestions for how to track the user sessions and then invalidate them on demand?

Thank you!

James Allen
  • 93
  • 1
  • 5

1 Answers1

6

What I did for a similar situation is to use a custom session ID that includes the user id in the key name. Maybe there's an easier way to do this now, but here's basically what I had to do to set a custom session id:

main.js:

var uid = require('uid'),
    redis = require('redis'),
    session = require('express-session'),
    RedisStore = require('connect-redis')(session),
    Session = session.Session,
    Cookie = session.Cookie;

var utils = require('./utils');

var COOKIE_SECRET = 'somethingrandom',
    COOKIE_KEY = 'mycustomsession';

var redisClient = redis.createClient(),
    redisStore = new RedisStore({
      client: redisClient,
      ttl: 24 * 60 * 60, // 1 day session expiration
      prefix: 'sess:'
    });

// ... then inside the login route after user was successfully authenticated ...
req.sessionStore = redisStore;
req.sessionID = 'sess:' + user.id + ':' + uid(24);
req.session = new Session(req);
req.session.cookie = new Cookie({});
req.session.user = user;
utils.createSession(req, res, COOKIE_KEY, COOKIE_SECRET);

utils.js:

var onHeaders = require('on-headers'),
    signature = require('cookie-signature');
exports.createSession = function(req, res, name, secret) {
  var trustProxy = true;
  // ripped from express-session
  onHeaders(res, function() {
    if (!req.session)
      return;

    var cookie = req.session.cookie
      , proto = (req.headers['x-forwarded-proto'] || '').split(',')[0].toLowerCase().trim()
      , tls = req.connection.encrypted || (trustProxy && 'https' == proto);

    // only send secure cookies via https
    if (cookie.secure && !tls)
      return;

    var val = 's:' + signature.sign(req.sessionID, secret);
    res.cookie(name, val, cookie.data);
  });

  // proxy end() to commit the session
  var end = res.end;
  res.end = function(data, encoding) {
    res.end = end;
    if (!req.session) return res.end(data, encoding);
    req.session.resetMaxAge();
    req.session.save(function(err) {
      if (err) console.error(err.stack);
      res.end(data, encoding);
    });
  };
};
mscdex
  • 104,356
  • 15
  • 192
  • 153
  • Thanks! How do you then expire all of a user's sessions? We store a lot of other data in Redis and doing a full scan using `KEYS` or `SCAN` isn't really feasible. We need to be able to do some sort of direct look up I think. – James Allen Oct 13 '14 at 15:47
  • I use `SCAN` with a pattern and haven't had any issues. However you could just use a separate database for sessions and `SCAN`, or make the redis key non-unique (keyed on a prefix + user id) and the value containing a JSON array of sessions for that user? – mscdex Oct 13 '14 at 16:52
  • Thanks, I think a combination of separate database and `SCAN` could indeed work in this case. Thanks for the answer! – James Allen Oct 14 '14 at 17:48