Prevent Expressjs from creating a session when requests contain an authorization header?
Asked Answered
R

4

8

I have an API that can be called either using a browser where requests are transactional and have a session OR directly, eg. using curl, where requests are atomic. Browser requests must first authenticate and then use an express session (connect.sid) for subsequent authorization, direct API calls use a header: Authorization: "SOMETOKEN" which has to be sent for every request.

The problem I have is, because I'm using the same web server to serve both atomic and transactional traffic, every API call is needlessly being given a session by Express. Every response includes a Set-Cookie and all these sessions are filling up my session store. Therefore: how can I prevent Express from entering a new sess key in the memory store (Redis) when a request contains an Authorization header?

Note. I get that a more classic approach would be to have a separate API server and a separate WEB server but why not run both on one machine? To me, the difference is that the API serves data and the WEB serves views but beyond that they are both part of the same application. I just happen to also allow users to access their data directly and don't force them to use my interface.

### Express Config

module.exports = function(app, exp, sessionStore, cookieParser, passport, flash) {
  app.configure(function() {
    // Templates
    app.set('views', ERNEST.root + '/server/views');
    app.set('view engine', 'jade');
    app.set('view options', { doctype : 'html', pretty : true });
    
    // Allow large files to be uploaded (default limit is 100mb)
    app.use(exp.limit('1000mb'));
    
    // Faux putting and deleting
    app.use(exp.methodOverride());
    
    // Static content
    app.use(exp.static(ERNEST.root + '/server'));
    app.use(exp.static(ERNEST.root + '/public'));
    
    // Handle favicon
    app.use(exp.favicon());
    
    // For uploads
    app.use(exp.bodyParser({keepExtensions: true}));
            
    // Configure cookie parsing
    if (cookieParser)   app.use(cookieParser);
    else app.use(exp.cookieParser());
    
    // Where to store the session
    var session_options = { 'secret': "and she put them on the mantlepiece" };
    if (sessionStore) session_options.store = sessionStore;
    app.use(exp.session(session_options));
    
    // Rememberance
    app.use(function (req, res, next) {
      if (req.method == 'POST' && req.url == '/authenticate') {
        if ( req.body.rememberme === 'on' ) {
          req.session.cookie.maxAge = 2592000000; // 30*24*60*60*1000 Rememeber 'me' for 30 days
        } else {
          req.session.cookie.expires = false;
        }
      }
      next();
    });
            
    // PassportJS
    if (passport){
      app.use(flash());
      app.use(passport.initialize());
      app.use(passport.session());
    }
  });
};

An example route:

app.get('/status/past_week', MID.ensureAuthenticated, MID.markStart, function(req, res) {
  WEB.getStatus('week', function(err, statuses){
    if (err) res.send(500, err);
    else res.send(200, statuses);
  });
});

Authorization Middleware

MID.ensureAuthenticated = function(req, res, next) {
  if (req.isAuthenticated()) return next();
  else {
    isAuthorised(req, function(err, authorised) {
      if (err) return res.redirect('/');
      else if (authorised) return next();
      else return res.redirect('/');
    });
  }
    
  function isAuthorised(req, callback) {
    var authHeader = req.headers.authorization;
    if (authHeader) {
      // Has header, verify it
      var unencoded = new Buffer(authHeader, 'base64').toString();
      var formatted = unencoded.toString().trim();
      ACCOUNT.verifyAuth(formatted, callback); // verifyAuth callbacks next() when successful
    } else callback(null, false); // No Authorised header
  }
};
Riotous answered 21/1, 2014 at 17:20 Comment(0)
W
19

Try this:

var sessionMiddleware = exp.session( session_options );

app.use(function(req, res, next) {
  if (req.headers.authorization) {
    return next();
  }
  return sessionMiddleware(req, res, next);
});
Whatsoever answered 23/1, 2014 at 18:28 Comment(0)
C
1

Looks like you need to write your own session middleware. Here's an example. If you can create a separate subdomain, say, www.example.com for browser sessions and app.example.com for accessing it directly, then you should be able to use the linked method almost exactly, and just don't start the session for app.example.com requests. That may be the most direct method whereby the call indicates the method it intends to authenticate by, and any diversion from that is an error.

Otherwise, you'll have to detect the authentication token in the middleware and not start the session when you find it.

Collectivize answered 23/1, 2014 at 18:25 Comment(1)
Thanks, your suggestion of using a separate subdomain makes a lot of sense; I suspect we will end up doing this and your link will be useful then.Riotous
U
1

An alternative approach to reducing the amount of sessions stored in your session storage is to set a default maxAge to something low. Then, when you actually need sessions stored longer, like after a user logins, you can set req.session.cookie.expires = null;. Also don't forget to set the session expiration to something low when the user logs out.

Here's an example:

// set default to something low
app.use(session({
  resave: true,
  saveUninitialized: true,
  cookie: {
    maxAge: 5 * 60 * 1000 // 5 minutes
  },
  secret: secrets.sessionSecret,
  store: new MongoStore({
    url: yourUrl,
    auto_reconnect: true
  })
}));

// on successful login, 
// set expiration to null or something longer than default
var time = 14 * 24 * 3600000; //2 weeks
req.session.cookie.maxAge = time;
req.session.cookie.expires = new Date(Date.now() + time);
req.session.touch();

// on logout, reset expiration to something low  
var time = 5 * 60 * 1000; // 5 minutes
req.session.cookie.maxAge = time; //2 weeks
req.session.cookie.expires = new Date(Date.now() + time);
req.session.touch();

This is particularly useful when remote monitoring your app because if the monitoring is frequent enough, the sessions will fill up fast.

Upstretched answered 25/10, 2014 at 21:19 Comment(0)
H
-1

You can always just catch the response headers event and remove the 'set-cookie' header:

app.use(function(req, res, next) {
  res.on('header', function () {
    if (req.headers.authorization) {
      delete res._headers['set-cookie'];
    }
  });
  next();
});

You can technically put this anywhere in your middleware chain.

Hebner answered 21/1, 2014 at 19:0 Comment(2)
To be fair, you've directly answered the question but this won't work. It does successfully remove the set-cookie header but that's not my real problem. My real problem is all the sess keys being created in my memory store (Redis), one per request - this quickly eats memory. I'll update the question to reflect this.Riotous
Using this approach of checking each req, one option is to use req.session.destroy(); to remove the session right after Express has set it. I'd rather prevent Express from setting it in the first place though.Riotous

© 2022 - 2024 — McMap. All rights reserved.