Session: cookie.expires goes back to original value on each request

Created on 27 Jul 2015  路  27Comments  路  Source: expressjs/session

I'm having a problem with getting the session to refresh. I'm using express 4 with the latest express-session and backed with a redis store. I can see the redis ttls getting properly refreshed. The core session cookie expire and maxAge have a strange behavior. They updates fine after the req.session.touch() call, however on the next request, they go back to their previous values, and eventually expire the cookie even during activity.

My configuration:

{
      "name": "sid",
      "secret": "some-secret",
      "rolling": true,
      "saveUninitialized": false,
      "resave": false,
      "cookie": {
        "maxAge": 60000 <-- 60 seconds for the sake of demoing this issue
      },
      "storeParams": {
        "host": "localhost",
        "port": "6379"
      }
}

Any ideas what could be wrong? do I have to explicitly reset the session cookie expire myself on each request?

Most helpful comment

In our case, we addressed the symptom by setting resave: true.

The issue appeared to be that, when rolling: true:

  1. the middleware would touch the session (req.session.touch(), which alters the cookie value only)
  2. fail to save the session (shouldSave(req) being false, as it ignores changes in the cookie value itself).
  3. succeed in touching the session, bumping the redis TTL.

The end result being a cookie store that does not update its cookie expirations, but does update their TTLs.

resave: true forces shouldSave(req) to true, and overcomes this.

All 27 comments

Any ideas what could be wrong?

would you test different combinations of the rolling, saveUninitialized, and resave options and see if the issue continues to exist?

rolling is your issue.

Force a cookie to be set on every response. This resets the expiration date.

We may want to add This resets the expiration date on the cookie to the given default.


So, @simoami to understand what is happening better are you trying to set the cookie expires to like 90000 but it's resetting to the 60000 (as per your example)? or are you trying to set it to 60000 but getting something else?

Here's the screen capture of the debug session. I'm going to test with rolling set to false.

http://cl.ly/2Z15352f3z2H

WOW, that's weird.

Yep, very weird. Setting rolling to false doesn't help unfortunately. I also tried with the rolling, saveUninitialized, and resave unspecified in the config.

Here's a debug log for analysis:

Config:

{
      "secret": "some-secret",
      "cookie": {
        "maxAge": 60000
      },
      "storeParams": {
        "host": "localhost",
        "port": "6379"
      }
}

Log:

GET /vendors/list 200 66.511 ms - -
Mon, 27 Jul 2015 16:31:19 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:19 GMT express-session session found
Mon, 27 Jul 2015 16:31:19 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:19 GMT express-session split response

GET /vendors/list 200 70.500 ms - -
Mon, 27 Jul 2015 16:31:24 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:24 GMT express-session session found
Mon, 27 Jul 2015 16:31:24 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC

GET /vendors/list 200 73.383 ms - -
Mon, 27 Jul 2015 16:31:24 GMT express-session split response
Mon, 27 Jul 2015 16:31:28 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:28 GMT express-session session found
Mon, 27 Jul 2015 16:31:28 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:28 GMT express-session split response

GET /vendors/list 200 74.058 ms - -
Mon, 27 Jul 2015 16:31:33 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:33 GMT express-session session found
Mon, 27 Jul 2015 16:31:33 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:33 GMT express-session split response

GET /vendors/list 200 68.059 ms - -
Mon, 27 Jul 2015 16:31:36 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:36 GMT express-session session found
Mon, 27 Jul 2015 16:31:36 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:36 GMT express-session split response

GET /vendors/list 200 65.760 ms - -
Mon, 27 Jul 2015 16:31:41 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:41 GMT express-session session found

GET /vendors/list 200 79.143 ms - -
Mon, 27 Jul 2015 16:31:41 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:41 GMT express-session split response
Mon, 27 Jul 2015 16:31:45 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:45 GMT express-session session found

GET /vendors/list 200 85.360 ms - -
Mon, 27 Jul 2015 16:31:45 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:45 GMT express-session split response
Mon, 27 Jul 2015 16:31:51 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:51 GMT express-session session found
Mon, 27 Jul 2015 16:31:51 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:51 GMT express-session split response

GET /vendors/list 200 63.701 ms - -
Mon, 27 Jul 2015 16:31:55 GMT express-session fetching YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:55 GMT express-session session found
Mon, 27 Jul 2015 16:31:55 GMT express-session saving YfF3yWXIa_apvMpOB5AThP-2Jbb6P_jC
Mon, 27 Jul 2015 16:31:55 GMT express-session split response

GET /vendors/list 200 60.398 ms - -
Mon, 27 Jul 2015 16:32:00 GMT express-session no SID sent, generating session

GET /vendors/list 302 27.626 ms - 13
Mon, 27 Jul 2015 16:32:00 GMT express-session saving 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session split response
Mon, 27 Jul 2015 16:32:00 GMT express-session set-cookie connect.sid=s%3A0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc.nmKJrui2mA6Me5CEVE933jEw%2BrFsbmNER%2Brw%2FsJN4AI; Path=/; Expires=Mon, 27 Jul 2015 16:33:00 GMT; HttpOnly
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session session found

**************** SESSION EXPIRED, PAGE RELOAD ******************

GET /locations 302 26.508 ms - 68
Mon, 27 Jul 2015 16:32:00 GMT express-session saving 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session split response
Mon, 27 Jul 2015 16:32:00 GMT express-session set-cookie connect.sid=s%3A0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc.nmKJrui2mA6Me5CEVE933jEw%2BrFsbmNER%2Brw%2FsJN4AI; Path=/; Expires=Mon, 27 Jul 2015 16:33:00 GMT; HttpOnly
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session session found
Mon, 27 Jul 2015 16:32:00 GMT express-session saving 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session split response

GET /login 200 30.271 ms - -
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session fetching 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:00 GMT express-session session found
Mon, 27 Jul 2015 16:32:00 GMT express-session session found
Mon, 27 Jul 2015 16:32:00 GMT express-session session found
Mon, 27 Jul 2015 16:32:01 GMT express-session session found
Mon, 27 Jul 2015 16:32:01 GMT express-session session found
Mon, 27 Jul 2015 16:32:01 GMT express-session saving 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc
Mon, 27 Jul 2015 16:32:01 GMT express-session saving 0-kTd__wLMm6Ox1J0IPXdZKeAFc3a3Lc

This seems related to #174 you may want to try expires instead of maxAge and I think you have to add Date.now() + offset

Thanks Gabe,
Are you suggesting to set the expire value as part of the initial config? or on each request in some function(req, res, next) middleware?

I don't remember which was the issue... Try either expires or maxAge for both but I don't suggest mixing them (possibly more headaches) but for resetting definitely add Date.now() + 60000 & for the default config.

So from the cookie module:

expires:
absolute expiration date for the cookie (Date object)

&

maxAge:
relative max age of the cookie from when the client receives it (seconds)

I played with the params as suggested by @JoeWagner and got a working configuration. The offending setting was saveUnitinialized. When set to false, it causes the session to expire despite subsequent requests. The rolling config was needed because it sends http header cookie to the browser with updated expires, so the browser doesn't dispose of the sessionid on expire. The rolling configuration ensures the browser also extends its reference to the session id. it would be good to update the documentation to reflect this behavior. my working config:

{
  "name": "sid",
  "secret": "some-secret",
  "rolling": true,
  "saveUninitialized": true,
  "resave": false,
  "cookie": {
    "maxAge": 20000
  },
  "storeParams": {
    "host": "localhost",
    "port": "6379"
  }
}

I still want to investigate what saveUninitialized does really. The docs say "A session is uninitialized when it is new but not modified", but it fails to explain what is a modified session? it a session modifeid when data sas been inserted into it?

@simoami understand then that saveUninitialized means that any user who isn't logged in yet and is on a page with session loaded will get a session & a database entry... a modified session is a session you have added/deleted something from usually on a new session you can only add something as nothing is there.

Thanks @gabeio

a modified session is a session you have added/deleted something from usually on a new session you can only add something as nothing is there

This would be valuable info to add to the documentation, under saveUnititialized.

Also, I forgot to mention that I had the expire set as you recommended. I still get session issues when a try removing that expires code. so I'm taking back my conclusion on root cause. will need more time to analyze.

@simoami anything new to report on the session issues you mentioned in your last post?

@gabeio unfortunately, I'm a bit short on time and can't help debug this further. I think there's enough info on this thread to help reproduce and fix.

There is not enough information on this thread to reproduce. I have personally tried, but have not been able to. Please either provide the fix as a pull request, or provide the following for us to reproduce (I'm just listing everything we need, even is some things were given above):

  1. The exact version of this module
  2. The exact version of Node.js
  3. The exact versions of all other modules needs to reproduce.
  4. Full, complete source code we can copy and paste into a file and run.
  5. Full, complete instructions on how to reproduce. Please leave nothing implied, as everything you just leave to implied makes it that much more unlikely for us to reproduce. For example, saying "make a request to the server" is not enough information. What does the cookie jar in the browser contain when the request is made, for example?

@dougwilson Thanks for the instructions. I'll try to spare some time to reproduce again. need my memory freshened again on the issue and will get back to you. At the time I logged the issue, I was using Node v0.12 and express 4.13.1 and express-session 1.11.3. my config for the middleware:

    "params": {
      "name": "sid",
      "secret": "***",
      "rolling": true,
      "saveUninitialized": true,
      "resave": true,
      "cookie": {
        "maxAge": 1200000
      },
      "storeParams": {
        "host": "redis",
        "port": "6379"
      }
    }

Note: please keep in mind that the settings above have been changed in every combination possible. i.e. with resave false and/or saveUninitialized set to false. I've also tried with and without redis as store.

This is my session middleware:

'use strict';

/**
 * Middleware to add session handling with a redis store.
 */

module.exports = function sessionHandler(config) {

  var app = config.app;

  var services = config.appConfig.services;

  var session = require('express-session');
  var RedisStore = require('connect-redis')(session);

  // SESSION MIDDLEWARE

  //config.genid = function(req) {
  //  return req.accessToken ? req.accessToken.id : null;
  //};

  // default maxAge to 20 minutes to milliseconds
  //var maxAge = (app.get('session') || {}).maxAge ||  10 * 1000;
  var params = (config || {}).params || {};

  var maxAge = (params.cookie||{}).maxAge || 60000;

  config.cookie = {
    expires: new Date(Date.now() + maxAge),
    maxAge: maxAge
  };

  var storeParams = params.storeParams;

  delete params.storeParams;

  params.store = new RedisStore(services.redis || storeParams);

  if (app.get('env') === 'production' && params.httpsEnabled) {
    app.set('trust proxy', 1); // trust first proxy
    // IMPORTANT: setting secure to true requires an https connection
    params.cookie.secure = true; // serve secure cookies
  }
  return session(params);
};

Hi @simoami what version of connect-redis should we use? Which version of Redis should we connect to? I see you said you also tried without Redis. What did you use instead? Did you just use the memory store?

Typically to reproduce, it comes down to know the exact requests to make extremely important. Almost all issues like this, it's critical to know what the contents of your web browser's cookie jar looks like, as so far, every issue like this has been resolved with clearing all the browser cookies, since the cookie jar was in a state such that it didn't accept new cookies from this module (perhaps because there was an overlapping that was preferred over the new cookie, so the browser kept sending the old cookie, even when setting the new one).

Please provide the exact steps of requests to make to reproduce the issue. Ideally start the steps from a brand new browsing session, with all cookies and cache cleared if you don't completely understand the current browser state and the algorithms behind browser cookie jars.

Please let me know once you provide the instructions so we can proceed :)

@dougwilson That makes sense. I know what you mean when it comes to reproducing issues.

This is my dependency list:

"dependencies": {
    "async": "^1.4.0",
    "body-parser": "^1.13.2",
    "compression": "^1.5.1",
    "connect-redis": "^2.3.0",
    "cookie-parser": "^1.3.5",
    "csurf": "^1.8.3",
    "debug": "^2.2.0",
    "ejs": "^2.3.2",
    "errorhandler": "^1.4.1",
    "express": "^4.13.1",
    "express-session": "^1.11.3",
    "express-validator": "^2.13.0",
    "knox": "^0.9.2",
    "less-middleware": "^2.0.1",
    "method-override": "^2.3.3",
    "middle-earth": "0.0.6",
    "moment": "^2.10.3",
    "moment-timezone": "^0.4.0",
    "morgan": "^1.6.1",
    "multer": "^0.1.8",
    "multiparty": "^4.1.2",
    "readable-stream": "^2.0.1",
    "recluster": "^0.4.0",
    "seneca": "^0.6.2",
    "seneca-nsq-transport": "^1.0.0",
    "serve-favicon": "^2.3.0"
  },

Here's my middleware sequence:

{
  "serve-favicon": false,
  "compression": {},
  "cookie-parser": {
    "params": "secret"
  },
  "./session": {
    "params": {
      "name": "sid",
      "secret": "secret",
      "rolling": true,
      "saveUninitialized": true,
      "resave": true,
      "cookie": {
        "maxAge": 1200000
      },
      "storeParams": {
        "host": "redis",
        "port": "6379"
      }
    }
  },
  "body-parser#json": {
    "params": {
      "strict": false
    }
  },
  "body-parser#urlencoded": {
    "params": {
      "extended": true
    }
  },
  "method-override": [
    { },
    { "params": "_method" }
  ],
  "csurf": {},
  "./context": {},
  "express-validator": {},
  "./static":  {
    "params": "client/www"
  },
  "./404": {},
  "./500": {}
}

I'll see if I can find some time to maybe setup a separate app with a bare minimum deps to test this. It's hard to troubleshoot when you have multiple middleware components.

Hi @simoami , the copy of your package.json does not actually show what specific versions of the modules you are using is, especially since they all start with ^. I'm not sure what "Here's my middleware sequence" means. I've never seen that before. Should it mean something to me? Does it connect up with some module I should know about?

Other than those questions, did you find a bare minimum app to test this?

Typically to reproduce, it comes down to know the exact requests to make extremely important. Almost all issues like this, it's critical to know what the contents of your web browser's cookie jar looks like, as so far, every issue like this has been resolved with clearing all the browser cookies, since the cookie jar was in a state such that it didn't accept new cookies from this module (perhaps because there was an overlapping that was preferred over the new cookie, so the browser kept sending the old cookie, even when setting the new one).

Please provide the exact steps of requests to make to reproduce the issue. Ideally start the steps from a brand new browsing session, with all cookies and cache cleared if you don't completely understand the current browser state and the algorithms behind browser cookie jars.

Please let me know once you provide the instructions so we can proceed :)

I have experienced the same issue with a SPA and express-session backend. Using the suggested config by @simoami solved the issue. I could see from logging the req.session that my issue was a negative originalMaxAge. Any clues as to how to prevent this issue in the future besides copying some config?

In our case, we addressed the symptom by setting resave: true.

The issue appeared to be that, when rolling: true:

  1. the middleware would touch the session (req.session.touch(), which alters the cookie value only)
  2. fail to save the session (shouldSave(req) being false, as it ignores changes in the cookie value itself).
  3. succeed in touching the session, bumping the redis TTL.

The end result being a cookie store that does not update its cookie expirations, but does update their TTLs.

resave: true forces shouldSave(req) to true, and overcomes this.

I have got same problem :( data after authentication writes to database and saves to request. But after first access to rest api they get nulled.

+1 @igrayson

I have also had to set resave:true to get rolling to work in my case. I am using connect-mongo as my store also.

Hi everyone, this issue is old, a lot of information is missing from my questions to gather it, and it seems like the OP has abandoned this issue. If you are still having this issue, please open a new issue in this repository, and provide all the information I was asking for above, thank you!

Hi! Please correct me, do I understand the process right?

Let's say we want to reduce maxAge. We use rolling: true option.
We change maxAge req.session.maxAge = 60000 and express-session count new expires and update store with it (I use connect-mongo).
But we do not rewrite whole serialised session (because we only perform touch. Touch realisation in mongo-connect).
Further we sent headers and cookie on client renewed with new expires.
Than we get new request with cookie id.
Express-session obtains old serialised session from store (mongo) and change expires according to maxAge that we actually set from the first beginning.
So, rolling option is ok for extending cookie expiration.
But if I want to reduce expiration time I need to use or resave: true or req.session.save() for actually rewrite serialised session.
?

After trying most solutions here, I wanted to try addressing the issue at the source:

Edit node_modules/express-session/index.js:
Change: (near line 243)
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
To:
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data, opts);

And,
Change: (near line 651)

function setcookie(res, name, val, secret, options) {
  var signed = 's:' + signature.sign(val, secret);
  var data = cookie.serialize(name, signed, options);

To:

function setcookie(res, name, val, secret, options, globalOpts) {
  var signed = 's:' + signature.sign(val, secret);
  var y2038 = '2038-01-19T03:14:05'
  var expiresOverride = globalOpts.expiresOverride || new Date (y2038)
  debug('index.js: setcookie(): NOTE: overriding "Expires=" field to %s', ''+expiresOverride);
  options.expires = expiresOverride
  var data = cookie.serialize(name, signed, options);

This adds an extra option expiresOverride that accepts a Date object to set the Expires field. It defaults to the date 2038-01-19T03:14:05 if not specified, and always overrides the Expires field.

Example usage:

app.use(session({
    . . .
    resave: false,
    saveUninitialized: true,
    rolling: true,
    expiresOverride: new Date(Date.now() + 31536000000),  // 1 year
    cookie: {
        . . .
    },
}));

(Since I was using the AWS dynamodb-store module, I also had to update the "ttl" option for it, not shown here)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aheyer picture aheyer  路  15Comments

Jpunt picture Jpunt  路  17Comments

nhitchins picture nhitchins  路  20Comments

neutron92 picture neutron92  路  20Comments

fibo picture fibo  路  22Comments