session icon indicating copy to clipboard operation
session copied to clipboard

Cookieless Session

Open npshubh opened this issue 7 years ago • 20 comments
trafficstars

How can we implement cookie-less session in express js? Express-session provides a way to maintain session using cookies but what if we try to use express-session without cookies? Is there any way to achieve that?

npshubh avatar Jan 23 '18 04:01 npshubh

You could just call the get and set methods on your store object directly.

dougwilson avatar Jan 23 '18 04:01 dougwilson

can you give me an example for that? How I'm supposed to do that?

npshubh avatar Jan 23 '18 04:01 npshubh

So when you create the store object using your chosen store module, that module has various methods to interact with it like get, set, and more. Depending on how you're going to handle management of sessions without cookies, you would interact with the store module. This module is basically a "cookie adapter" for all the various stores. You would need to implement your own adapter over the stores to provide the functionality you're looking for.

dougwilson avatar Jan 23 '18 04:01 dougwilson

@dougwilson , when another user requests login using API, the first session stored in database is getting updated. So I'm losing session of previous user who was logged in using API. I'm unable to insert new session for each login request.

I can write custom code to achieve that but it worths to ask you if this library consists some logic to operate on such kind of scenario?

npshubh avatar Jan 23 '18 04:01 npshubh

Hm, I've never seen that happen here. It would be a big bug for sure. Can you help me reproduce the issue? I can take a look. Please provide all the following:

  1. Version of Node.js
  2. Version of this module
  3. Complete server code that reproduces the issue
  4. Instructions on how to get the server running if it is more than copy and paste your provided code
  5. Instructions on how to reproduce in tge browser. Usually (1) go to this address (2) click this link, etc.

Thanks!

dougwilson avatar Jan 23 '18 05:01 dougwilson

Node :- v9.2.1 "express-session": "^1.15.6",

Code is on my localhost so I can't show you but I can explain you the process -

  1. I've used following code to setup

app.use(session({ secret: "test", cookies: { maxAge: 600000,httpOnly: false }, saveUninitialized: false, // don't create session until something stored resave: false, //don't save session if unmodified store: new MongoStore({ mongooseConnection: mongoose.connection, autoRemove: 'interval', autoRemoveInterval: 10 }) }));

  1. Now whenever a user "A" calls API "locahost/login" I'm setting a token "req.session.token = 'secret_token'; " and I get data in my mongodb database. Till now everything is fine, we have successfully created a session for user "A".

  2. Now when another user "B" calls API "locahost/login" and tries to login, then session of user "A" gets updated. So if user "A" tries to login using his token, then I can't find that in db and user "A" is not able to login. What I wanted is to create different sessions for multiple users but I ended up with updating the same session.

This library seems to work fine with browser login but not working with API logins.

Hope now you've understood the problem.

npshubh avatar Jan 23 '18 05:01 npshubh

I kinda follow, but not sure why that is happening in your code. I would love to attach a debugger and walk though it and see what is going on, if you're willing to provide the requested information. If not, I'm not sure how I can figure out what is happening.

dougwilson avatar Jan 23 '18 05:01 dougwilson

gateway.zip here's the code.

npshubh avatar Jan 23 '18 05:01 npshubh

Awesome, thanks! I just got to bed, so will check it out when off work tomorrow night (approx in 20 hours).

dougwilson avatar Jan 23 '18 05:01 dougwilson

(I also have the issue open and labeled in case someone else has time to look sooner)

dougwilson avatar Jan 23 '18 05:01 dougwilson

@dougwilson thanks for your help.

npshubh avatar Jan 23 '18 05:01 npshubh

Hi @dougwilson, any update?

npshubh avatar Jan 30 '18 08:01 npshubh

I haven't had an opportunity yet. I have the issue open and labeled in case someone else has time to look sooner.

dougwilson avatar Jan 30 '18 13:01 dougwilson

It's been a while! What happened?

MatehElismar avatar Sep 27 '19 03:09 MatehElismar

@npshubh I am able to understand the problem,I wanted you to clarify the following

  1. Can you try with only one user(A), that is log in using API and do something and log out. And again the user(A) should to log in. This is to see whether the session is been deleted immediately after user(A) logged out or it remains in database.

  2. User(A) logs in, spend some time after logging in. Because as per your session settings, just to check that cookie would have been expired.

  3. Can please provide me minimal code to reproduce this issue.

HarshithaKP avatar Nov 21 '19 09:11 HarshithaKP

Regarding the problem of not being able to store more than one user in the session database. I had a similar problem during testing. It was a mix of misconfigured authentication and me repeatedly sending the same cookie through postman over and over again which bypassed the signin route. :pensive:

I'm still interested in a solution regarding sessions without cookies. My use case is using session based auth on a react native expo frontend which doesn't support cookies.

  • Codedamn on youtube describes the problem and a middleware solution for handling incoming requests. However, Codedamn does not describe how to save the cookie into a header.
  • According to an old github question, express session doesn't allow access to the cookie in the response.
  1. Have there been any updates to express session in recent years allowing access the cookie before the response is sent (thereby allowing the cookie to be put in a header)?
  2. Is there any other known way of putting the cookie in the headers?

SimonAM avatar May 29 '21 18:05 SimonAM

I created an ugly and temporary solution.

In express-sessions/index.js, in the session function i add the option replaceCookieWithAccessToken with a default as false. In the setcookie function i add a new parameter: isCookieReplacedWithToken taking the above option as an argument. I then add an if-else statement in the setcookie function:

  if(isReplaceCookieWithToken){
 	res.setHeader('X-Access-Token', header)  
 } else {
 	 res.setHeader('Set-Cookie', header)
 }

This seems to work, but i really have no idea if it breaks some other part of the code. If there are no other alternatives for creating cookieless sessions i hope someone could integrate a similar solution into the project.

SimonAM avatar May 30 '21 06:05 SimonAM

I have found this library https://www.npmjs.com/package/express-session-header

Fork a express-session with support auth by header instead cookie. Unstable. Do not use it at production

usage

var session= require('express-session-header ');

app.use( session({
      cookie: null, // important! set to null for enable header mode
      header: 'x-auth', // important! set a header name
      secret: 'secret',
      resave: false,
      saveUninitialized: false,
}))

it's just a try of some guy to make a cookiless session, but don't works

this is the code

/*!
 * express-session
 * Copyright(c) 2010 Sencha Inc.
 * Copyright(c) 2011 TJ Holowaychuk
 * Copyright(c) 2014-2015 Douglas Christopher Wilson
 * MIT Licensed
 */

'use strict';

/**
 * Module dependencies.
 * @private
 */

var Buffer = require('safe-buffer').Buffer
var cookie = require('cookie');
var crypto = require('crypto')
var debug = require('debug')('express-session');
var deprecate = require('depd')('express-session');
var onHeaders = require('on-headers')
var parseUrl = require('parseurl');
var signature = require('cookie-signature')
var uid = require('uid-safe').sync

var Cookie = require('./session/cookie')
var MemoryStore = require('./session/memory')
var Session = require('./session/session')
var Store = require('./session/store')

// environment

var env = process.env.NODE_ENV;

/**
 * Expose the middleware.
 */

exports = module.exports = session;

/**
 * Expose constructors.
 */

exports.Store = Store;
exports.Cookie = Cookie;
exports.Session = Session;
exports.MemoryStore = MemoryStore;

/**
 * Warning message for `MemoryStore` usage in production.
 * @private
 */

var warning = 'Warning: connect.session() MemoryStore is not\n'
  + 'designed for a production environment, as it will leak\n'
  + 'memory, and will not scale past a single process.';

/**
 * Node.js 0.8+ async implementation.
 * @private
 */

/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

/**
 * Setup session store with the given `options`.
 *
 * @param {Object} [options]
 * @param {Object} [options.cookie] Options for cookie
 * @param {Function} [options.genid]
 * @param {String} [options.name=connect.sid] Session ID cookie name
 * @param {Boolean} [options.proxy]
 * @param {Boolean} [options.resave] Resave unmodified sessions back to the store
 * @param {Boolean} [options.rolling] Enable/disable rolling session expiration
 * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
 * @param {String|Array} [options.secret] Secret for signing session ID
 * @param {Object} [options.store=MemoryStore] Session store
 * @param {String} [options.unset]
 * @return {Function} middleware
 * @public
 */

function session(options) {
  var opts = options || {}

  // get the cookie options
  var cookieOptions = opts.cookie || {}

  // get the session id generate function
  var generateId = opts.genid || generateSessionId

  // get the session cookie name
  var name = opts.name || opts.key || 'connect.sid'

  // get the session store
  var store = opts.store || new MemoryStore()

  // get the trust proxy setting
  var trustProxy = opts.proxy

  // get the resave session option
  var resaveSession = opts.resave;

  // get the rolling session option
  var rollingSessions = Boolean(opts.rolling)

  // get the save uninitialized session option
  var saveUninitializedSession = opts.saveUninitialized

  var isCookieConfigurationSet = opts.cookie !== null;
  var headerName = opts.header;
  debug("headerName: ",headerName);
  var headerNameNormalized = headerName? headerName.toLowerCase() : null;
  debug("headerNameNormalized: ",headerNameNormalized);

  // get the cookie signing secret
  var secret = opts.secret

  if (typeof generateId !== 'function') {
    throw new TypeError('genid option must be a function');
  }

  if (resaveSession === undefined) {
    deprecate('undefined resave option; provide resave option');
    resaveSession = true;
  }

  if (saveUninitializedSession === undefined) {
    deprecate('undefined saveUninitialized option; provide saveUninitialized option');
    saveUninitializedSession = true;
  }

  if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') {
    throw new TypeError('unset option must be "destroy" or "keep"');
  }

  // TODO: switch to "destroy" on next major
  var unsetDestroy = opts.unset === 'destroy'

  if (Array.isArray(secret) && secret.length === 0) {
    throw new TypeError('secret option array must contain one or more strings');
  }

  if (secret && !Array.isArray(secret)) {
    secret = [secret];
  }

  if (!secret) {
    deprecate('req.secret; provide secret option');
  }

  // notify user that this store is not
  // meant for a production environment
  /* istanbul ignore next: not tested */
  if ('production' == env && store instanceof MemoryStore) {
    console.warn(warning);
  }

  // generates the new session
  store.generate = function(req){
    req.sessionID = generateId(req);
    req.token = "s:"+signature.sign(req.sessionID,secret[0]);
    req.session = new Session(req);

    if(isCookieConfigurationSet) {
      req.session.cookie = new Cookie(cookieOptions);
    }

    if (isCookieConfigurationSet && cookieOptions.secure === 'auto') {
      req.session.cookie.secure = issecure(req, trustProxy);
    }
  };

  var storeImplementsTouch = typeof store.touch === 'function';

  // register event listeners for the store to track readiness
  var storeReady = true
  store.on('disconnect', function ondisconnect() {
    storeReady = false
  })
  store.on('connect', function onconnect() {
    storeReady = true
  })

  return function session(req, res, next) {
    // self-awareness
    if (req.session) {
      next()
      return
    }

    // Handle connection as if there is no session if
    // the store has temporarily disconnected etc
    if (!storeReady) {
      debug('store is disconnected')
      next()
      return
    }

    if (isCookieConfigurationSet) {
      // pathname mismatch
      var originalPath = parseUrl.original(req).pathname || '/'
      if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next();
    }

    // ensure a secret is available or bail
    if (!secret && !req.secret) {
      next(new Error('secret option required for sessions'));
      return;
    }

    // backwards compatibility for signed cookies
    // req.secret is passed from the cookie parser middleware
    var secrets = secret || [req.secret];

    var originalHash;
    var originalId;
    var savedHash;
    var touched = false

    // expose store
    req.sessionStore = store;
    var cookieId;

    if (isCookieConfigurationSet) {
      // get the session ID from the cookie
      cookieId = req.sessionID = getcookie(req, name, secrets);
    }

    if (headerName) {
      debug("Set using header with name: ", headerName);
      // get the session ID from the header
      // Allow previous middleware to set session ID
      cookieId = req.sessionID || (req.sessionID = getHeader(req, headerNameNormalized, secrets[0]));
	  req.token = req.token || "s:"+signature.sign(req.sessionID,secret[0]);
      debug("Got cookie id", cookieId);
    }

    // set-cookie
    onHeaders(res, function(){
      if (!req.session) {
        debug('no session');
        return;
      }

      if (!shouldSetCookie(req)) {
        return;
      }

      if (isCookieConfigurationSet) {
      // only send secure cookies via https
        if (req.session.cookie.secure && !issecure(req, trustProxy)) {
          debug('not secured');
          return;
        }
        if (!shouldSetCookie(req)) {
          return;
        }
        // set cookie
        setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
      }

      if (!touched) {
        // touch session
        req.session.touch()
        touched = true
      }
      if (headerName) {
        setHeader(res, headerName, req.sessionID, secrets[0]);
      }
    });

    // proxy end() to commit the session
    var _end = res.end;
    var _write = res.write;
    var ended = false;
    res.end = function end(chunk, encoding) {
      if (ended) {
        return false;
      }

      ended = true;

      var ret;
      var sync = true;

      function writeend() {
        if (sync) {
          ret = _end.call(res, chunk, encoding);
          sync = false;
          return;
        }

        _end.call(res);
      }

      function writetop() {
        if (!sync) {
          return ret;
        }

        if (chunk == null) {
          ret = true;
          return ret;
        }

        var contentLength = Number(res.getHeader('Content-Length'));

        if (!isNaN(contentLength) && contentLength > 0) {
          // measure chunk
          chunk = !Buffer.isBuffer(chunk)
            ? Buffer.from(chunk, encoding)
            : chunk;
          encoding = undefined;

          if (chunk.length !== 0) {
            debug('split response');
            ret = _write.call(res, chunk.slice(0, chunk.length - 1));
            chunk = chunk.slice(chunk.length - 1, chunk.length);
            return ret;
          }
        }

        ret = _write.call(res, chunk, encoding);
        sync = false;

        return ret;
      }

      if (shouldDestroy(req)) {
        // destroy session
        debug('destroying');
        store.destroy(req.sessionID, function ondestroy(err) {
          if (err) {
            defer(next, err);
          }

          debug('destroyed');
          writeend();
        });

        return writetop();
      }

      // no session to save
      if (!req.session) {
        debug('no session');
        return _end.call(res, chunk, encoding);
      }

      if (!touched) {
        // touch session
        req.session.touch()
        touched = true
      }

      if (shouldSave(req)) {
        req.session.save(function onsave(err) {
          if (err) {
            defer(next, err);
          }

          writeend();
        });

        return writetop();
      } else if (storeImplementsTouch && shouldTouch(req)) {
        // store implements touch method
        debug('touching');
        store.touch(req.sessionID, req.session, function ontouch(err) {
          if (err) {
            defer(next, err);
          }

          debug('touched');
          writeend();
        });

        return writetop();
      }

      return _end.call(res, chunk, encoding);
    };

    // generate the session
    function generate() {
      store.generate(req);
      originalId = req.sessionID;
      originalHash = hash(req.session);
      wrapmethods(req.session);
    }

    // wrap session methods
    function wrapmethods(sess) {
      var _reload = sess.reload
      var _save = sess.save;

      function reload(callback) {
        debug('reloading %s', this.id)
        _reload.call(this, function () {
          wrapmethods(req.session)
          callback.apply(this, arguments)
        })
      }

      function save() {
        debug('saving %s', this.id);
        savedHash = hash(this);
        _save.apply(this, arguments);
      }

      Object.defineProperty(sess, 'reload', {
        configurable: true,
        enumerable: false,
        value: reload,
        writable: true
      })

      Object.defineProperty(sess, 'save', {
        configurable: true,
        enumerable: false,
        value: save,
        writable: true
      });
    }

    // check if session has been modified
    function isModified(sess) {
      return originalId !== sess.id || originalHash !== hash(sess);
    }

    // check if session has been saved
    function isSaved(sess) {
      return originalId === sess.id && savedHash === hash(sess);
    }

    // determine if session should be destroyed
    function shouldDestroy(req) {
      return req.sessionID && unsetDestroy && req.session == null;
    }

    // determine if session should be saved to store
    function shouldSave(req) {
      // cannot set cookie without a session ID
      if (typeof req.sessionID !== 'string') {
        debug('session ignored because of bogus req.sessionID %o', req.sessionID);
        return false;
      }

      return !saveUninitializedSession && cookieId !== req.sessionID
        ? isModified(req.session)
        : !isSaved(req.session)
    }

    // determine if session should be touched
    function shouldTouch(req) {
      // cannot set cookie without a session ID
      if (typeof req.sessionID !== 'string') {
        debug('session ignored because of bogus req.sessionID %o', req.sessionID);
        return false;
      }

      return cookieId === req.sessionID && !shouldSave(req);
    }

    // determine if cookie should be set on response
    function shouldSetCookie(req) {
      // cannot set cookie without a session ID
      if (typeof req.sessionID !== 'string') {
        return false;
      }

      return cookieId != req.sessionID
        ? saveUninitializedSession || isModified(req.session)
        : rollingSessions || req.session.cookie.expires != null && isModified(req.session);
    }

    // generate a session if the browser doesn't send a sessionID
    if (!req.sessionID) {
      debug('no SID sent, generating session');
      generate();
      next();
      return;
    }

    // generate the session object
    debug('fetching %s', req.sessionID);
    store.get(req.sessionID, function(err, sess){
      // error handling
      if (err) {
        debug('error %j', err);

        if (err.code !== 'ENOENT') {
          next(err);
          return;
        }

        generate();
      // no session
      } else if (!sess) {
        debug('no session found');
        generate();
      // populate req.session
      } else {
        debug('session found');
        store.createSession(req, sess);
        originalId = req.sessionID;
        originalHash = hash(sess);

        if (!resaveSession) {
          savedHash = originalHash
        }

        wrapmethods(req.session);
      }

      next();
    });
  };
};

/**
 * Generate a session ID for a new session.
 *
 * @return {String}
 * @private
 */

function generateSessionId(sess) {
  return uid(24);
}

/**
 * Get the session ID cookie from request.
 *
 * @return {string}
 * @private
 */

function getcookie(req, name, secrets) {
  var header = req.headers.cookie;
  var raw;
  var val;

  // read from cookie header
  if (header) {
    var cookies = cookie.parse(header);

    raw = cookies[name];

    if (raw) {
      if (raw.substr(0, 2) === 's:') {
        val = unsigncookie(raw.slice(2), secrets);

        if (val === false) {
          debug('cookie signature invalid');
          val = undefined;
        }
      } else {
        debug('cookie unsigned')
      }
    }
  }

  // back-compat read from cookieParser() signedCookies data
  if (!val && req.signedCookies) {
    val = req.signedCookies[name];

    if (val) {
      deprecate('cookie should be available in req.headers.cookie');
    }
  }

  // back-compat read from cookieParser() cookies data
  if (!val && req.cookies) {
    raw = req.cookies[name];

    if (raw) {
      if (raw.substr(0, 2) === 's:') {
        val = unsigncookie(raw.slice(2), secrets);

        if (val) {
          deprecate('cookie should be available in req.headers.cookie');
        }

        if (val === false) {
          debug('cookie signature invalid');
          val = undefined;
        }
      } else {
        debug('cookie unsigned')
      }
    }
  }

  return val;
}

/**
 * Hash the given `sess` object omitting changes to `.cookie`.
 *
 * @param {Object} sess
 * @return {String}
 * @private
 */

function hash(sess) {
  // serialize
  var str = JSON.stringify(sess, function (key, val) {
    // ignore sess.cookie property
    if (this === sess && key === 'cookie') {
      return
    }

    return val
  })

  // hash
  return crypto
    .createHash('sha1')
    .update(str, 'utf8')
    .digest('hex')
}

/**
 * Determine if request is secure.
 *
 * @param {Object} req
 * @param {Boolean} [trustProxy]
 * @return {Boolean}
 * @private
 */

function issecure(req, trustProxy) {
  // socket is https server
  if (req.connection && req.connection.encrypted) {
    return true;
  }

  // do not trust proxy
  if (trustProxy === false) {
    return false;
  }

  // no explicit trust; try req.secure from express
  if (trustProxy !== true) {
    return req.secure === true
  }

  // read the proto from x-forwarded-proto header
  var header = req.headers['x-forwarded-proto'] || '';
  var index = header.indexOf(',');
  var proto = index !== -1
    ? header.substr(0, index).toLowerCase().trim()
    : header.toLowerCase().trim()

  return proto === 'https';
}

/**
 * Set cookie on response.
 *
 * @private
 */

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

  debug('set-cookie %s', data);

  var prev = res.getHeader('Set-Cookie') || []
  var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];

  res.setHeader('Set-Cookie', header)
}

/**
 * Verify and decode the given `val` with `secrets`.
 *
 * @param {String} val
 * @param {Array} secrets
 * @returns {String|Boolean}
 * @private
 */
function unsigncookie(val, secrets) {
  for (var i = 0; i < secrets.length; i++) {
    var result = signature.unsign(val, secrets[i]);

    if (result !== false) {
      return result;
    }
  }

  return false;
}


function setHeader(res, name, val, secrets) {
  var signed = 's:' + signature.sign(val, secrets);
  debug("setHeader signed: ",signed);
  debug(name + ' %s', signed);
  res.setHeader(name, signed);
}

function getHeader(req, name, secret) {
  var header = req.headers[name];
  debug("Header name: ", name)
  debug("Header value", header)
  var val;
   // read from header
  if (header) {
    if (header.substr(0, 2) === 's:') {
      val = signature.unsign(header.slice(2), secret);
      if (val === false) {
        debug('header signature invalid');
        val = undefined;
      }
    } else {
      debug('header unsigned')
    }
  }
  debug("getHeader val: ", val);
  return val;
}

@dougwilson take a look... maybe you can implement a cookieless session with a little bit of changes

cesco69 avatar Dec 22 '21 08:12 cesco69

This is a working header based session for express

import * as session from 'session';

app.use(session.session({
      store: sessionStore,
      secret: 'secret',
      resave: false,
      saveUninitialized: false,
      alwaysSetHeader: true, // set header on every response
      header: 'X-SESSION-ID', // header name
}));

session.ts

import { Request, Response, NextFunction } from 'express';
import { Buffer } from 'safe-buffer';
import * as crypto from 'crypto';
import * as onHeaders from 'on-headers';
import * as signature from 'cookie-signature';
import { sync as uid } from 'uid-safe';
import { EventEmitter } from 'events';


declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Express {
    interface Request {
      session?: Session;
      sessionID?: string;
      sessionStore?: Store;
      token?: string;
      secret?: string
    }
  }
}

/**
 * Expose constructors.
 */
export class Store extends EventEmitter {

  sessions: { [key: string]: string } = {};

  constructor() {
    super();
  }

  all(callback?: (err: any, sessions: { [key: string]: Session }) => void) {
    const sessionIds = Object.keys(this.sessions);
    const sessions: { [key: string]: Session } = {};

    for (let i = 0; i < sessionIds.length; i++) {
      const sessionId = sessionIds[i]
      const session = this.getSession(sessionId);

      if (session) {
        sessions[sessionId] = session;
      }
    }

    callback && setImmediate(callback, null, sessions);
  }

  clear(callback?: (err?: any) => void) {
    this.sessions = {};
    callback && setImmediate(callback)
  }

  get(sid: string, callback: (err: any, session?: Session) => void) {
    setImmediate(callback, null, this.getSession(sid))
  }

  set(sid: string, session: Session, callback?: (err?: any) => void) {
    this.sessions[sid] = JSON.stringify(session);
    callback && setImmediate(callback)
  }

  length(callback: (err: any, length: number) => void): void {
    this.all((err: any, sessions) => {
      if (err) {
        callback(err, null);
        return;
      }
      callback(null, Object.keys(sessions).length)
    })
  }

  touch(sid: string, session: Session, callback?: () => void) {
    const currentSession = this.getSession(sid);
    if (currentSession) {
      this.sessions[sid] = JSON.stringify(currentSession)
    }
    callback && setImmediate(callback);
  }

  destroy(sid: string, callback?: (err?: any) => void): void {
    delete this.sessions[sid];
    callback && setImmediate(callback);
  }

  /**
   * Re-generate the given requests's session.
   */
  regenerate(req: Request, callback: (err?: any) => any): void {
    this.destroy(req.sessionID, (err) => {
      (this as unknown as Store & { generate: (req: Request) => void }).generate(req);
      callback(err);
    });
  }

  /**
   * Load a `Session` instance via the given `sid`
   * and invoke the callback `fn(err, sess)`.
   */
  load(sid: string, callback: (err?: any, session?: Session) => any) {
    this.get(sid, (err, sess) => {
      if (err) return callback(err);
      if (!sess) return callback();
      const req = { sessionID: sid, sessionStore: this } as any;
      callback(null, this.createSession(req, sess))
    });
  }

  /**
   * Create session from JSON `sess` data.
   */
  createSession(req: Request, sess: Session) {
    req.session = new Session(req, sess);
    return req.session;
  }

  private getSession(sid: string): Session {
    const sess = this.sessions[sid]
    if (!sess) {
      return null;
    }
    return JSON.parse(sess);
  }
}


export class Session {

  public id: string;
  private req: Request;

  constructor(req: Request, data: unknown) {

    Object.defineProperty(this, 'req', {
      value: req,
      configurable: true,
      enumerable: false,
      writable: true
    });

    this.id = req.sessionID;

    if (typeof data === 'object' && data !== null) {
      // merge data into this, ignoring prototype properties
      for (const prop in data) {
        if (!(prop in this)) {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error
          this[prop] = data[prop];
        }
      }
    }
  }

  /**
   * Update reset session is still active.
   */
  touch(): this {
    return this;
  }

  /**
   * Save the session data with optional callback `fn(err)`.
   */
  save(callback?: (err: any) => void): this {
    this.req.sessionStore.set(this.id, this, callback || function () {
      // ...
    });
    return this;
  }

  /**
   * Re-loads the session data _without_ altering
   * the maxAge properties. Invokes the callback `fn(err)`,
   * after which time if no exception has occurred the
   * `req.session` property will be a new `Session` object,
   * although representing the same session.
   */
  reload(callback: (err?: any) => void): this {
    this.req.sessionStore.get(this.id, (err, sess) => {
      if (err) return callback(err);
      if (!sess) return callback(new Error('failed to load session'));
      this.req.sessionStore.createSession(this.req, sess);
      callback();
    });
    return this;
  }

  /**
   * Destroy `this` session.
   */
  destroy(callback: (err?: any) => void): this {
    delete this.req.session;
    this.req.sessionStore.destroy(this.id, callback);
    return this;
  }

  /**
   * Regenerate this request's session.
   */
  regenerate(callback: (err?: any) => void): this {
    this.req.sessionStore.regenerate(this.req, callback);
    return this;
  }
}



/**
 * Setup session store with the given `options`.
 */
export function session(options: { genid?: () => string, header?: string, secret?: string | string[], resave?: boolean, rolling?: boolean, saveUninitialized?: boolean, alwaysSetHeader?: boolean, unset?: string, store?: Store | any }): (req: Request, res: Response, next: NextFunction) => void {

  const opts = options || {};

  // get the session id generate function
  const generateId = opts.genid || generateSessionId;

  // get the session store
  const store = opts.store;
  if (store === undefined) {
    throw new TypeError('store option must be defined');
  }

  // get the resave session option
  let resaveSession = opts.resave;

  // get the rolling session option
  const rollingSessions = Boolean(opts.rolling);

  // get the save uninitialized session option
  let saveUninitializedSession = opts.saveUninitialized;

  let alwaysSetHeader = opts.alwaysSetHeader;

  const headerName = opts.header;
  if (headerName === undefined) {
    throw new TypeError('header option must be defined');
  }
  const headerNameNormalized = headerName ? headerName.toLowerCase() : null;

  let secret = opts.secret;

  if (typeof generateId !== 'function') {
    throw new TypeError('genid option must be a function');
  }

  if (resaveSession === undefined) {
    resaveSession = true;
  }

  if (saveUninitializedSession === undefined) {
    saveUninitializedSession = true;
  }

  if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') {
    throw new TypeError('unset option must be "destroy" or "keep"');
  }

  // switch to "destroy" on next major
  const unsetDestroy = opts.unset === 'destroy';

  if (Array.isArray(secret) && secret.length === 0) {
    throw new TypeError('secret option array must contain one or more strings');
  }

  if (secret && !Array.isArray(secret)) {
    secret = [secret];
  }

  // generates the new session
  store.generate = (req: Request) => {
    req.sessionID = generateId(req);
    req.token = `s:${signature.sign(req.sessionID, secret[0])}`;
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-expect-error
    req.session = new Session(req);
  };

  const storeImplementsTouch = typeof store.touch === 'function';

  // register event listeners for the store to track readiness
  let storeReady = true;
  store.on('disconnect', function ondisconnect() {
    storeReady = false
  })
  store.on('connect', function onconnect() {
    storeReady = true
  })

  return function session(req: Request, res: Response, next: NextFunction) {

    // self-awareness
    if (req.session) {
      next()
      return;
    }

    // Handle connection as if there is no session if
    // the store has temporarily disconnected etc
    if (!storeReady) {
      next();
      return;
    }

    // ensure a secret is available or bail
    if (!secret && !req.secret) {
      next(new Error('secret option required for sessions'));
      return;
    }

    // backwards compatibility for signed cookies
    // req.secret is passed from the cookie parser middleware
    const secrets = secret || [req.secret];

    let originalHash: string;
    let originalId: string;
    let savedHash: string;
    let touched = false;

    // expose store
    req.sessionStore = store;

    // get the session ID from the header
    // Allow previous middleware to set session ID
    const sessionId: string = req.sessionID || (req.sessionID = getHeader(req, headerNameNormalized, secrets[0]));
    req.token = req.token || (req.sessionID ? `s:${signature.sign(req.sessionID, secret[0])}` : undefined);


    // set-header
    onHeaders(res, () => {
      if (!req.session) {
        return;
      }

      if (!shouldSetHeader(req)) {
        return;
      }

      if (!touched) {
        // touch session
        req.session.touch()
        touched = true
      }
      setHeader(res, headerName, req.sessionID, secrets[0]);
    });

    // proxy end() to commit the session
    const _end = res.end;
    const _write = res.write;
    let ended = false;
    (res as any).end = function end(chunk: any, encoding: BufferEncoding) {
      if (ended) {
        return false;
      }

      ended = true;

      let ret: any;
      let sync = true;

      function writeend() {
        if (sync) {
          ret = _end.call(res, chunk, encoding);
          sync = false;
          return;
        }
        _end.call(res);
      }

      function writetop() {
        if (!sync) {
          return ret;
        }

        if (chunk == null) {
          ret = true;
          return ret;
        }

        const contentLength = Number(res.getHeader('Content-Length'));

        if (!isNaN(contentLength) && contentLength > 0) {
          // measure chunk
          chunk = !Buffer.isBuffer(chunk) ? Buffer.from(chunk, encoding) : chunk;
          encoding = undefined;

          if (chunk.length !== 0) {
            ret = _write.call(res, chunk.slice(0, chunk.length - 1));
            chunk = chunk.slice(chunk.length - 1, chunk.length);
            return ret;
          }
        }

        ret = _write.call(res, chunk, encoding);
        sync = false;

        return ret;
      }

      if (shouldDestroy(req)) {
        // destroy session
        store.destroy(req.sessionID, function ondestroy(err: any) {
          if (err) {
            setImmediate(next, err);
          }
          writeend();
        });

        return writetop();
      }

      // no session to save
      if (!req.session) {
        return _end.call(res, chunk, encoding);
      }

      if (!touched) {
        // touch session
        req.session.touch()
        touched = true
      }

      if (shouldSave(req)) {
        req.session.save(function onsave(err: any) {
          if (err) {
            setImmediate(next, err);
          }

          writeend();
        });

        return writetop();
      } else if (storeImplementsTouch && shouldTouch(req)) {
        // store implements touch method
        store.touch(req.sessionID, req.session, function ontouch(err: any) {
          if (err) {
            setImmediate(next, err);
          }
          writeend();
        });

        return writetop();
      }

      return _end.call(res, chunk, encoding);
    };

    // generate the session
    function generate() {
      store.generate(req);
      originalId = req.sessionID;
      originalHash = hash(req.session);
      wrapmethods(req.session);
    }

    // wrap session methods
    function wrapmethods(sess: Session) {
      const _reload = sess.reload;
      const _save = sess.save;

      function reload(callback: (...args: any) => void) {
        _reload.call(this, function (...args: any) {
          wrapmethods(req.session)
          callback.apply(this, args)
        })
      }

      function save(...args: any) {
        savedHash = hash(this);
        _save.apply(this, args);
      }

      Object.defineProperty(sess, 'reload', {
        configurable: true,
        enumerable: false,
        value: reload,
        writable: true
      })

      Object.defineProperty(sess, 'save', {
        configurable: true,
        enumerable: false,
        value: save,
        writable: true
      });
    }

    // check if session has been modified
    function isModified(sess: Session) {
      return originalId !== sess.id || originalHash !== hash(sess);
    }

    // check if session has been saved
    function isSaved(sess: Session) {
      return originalId === sess.id && savedHash === hash(sess);
    }

    // determine if session should be destroyed
    function shouldDestroy(req: Request) {
      return req.sessionID && unsetDestroy && req.session == null;
    }

    // determine if session should be saved to store
    function shouldSave(req: Request) {
      // cannot set cookie without a session ID
      if (typeof req.sessionID !== 'string') {
        return false;
      }
      return !saveUninitializedSession && sessionId !== req.sessionID ? isModified(req.session) : !isSaved(req.session)
    }

    // determine if session should be touched
    function shouldTouch(req: Request) {
      // cannot set cookie without a session ID
      if (typeof req.sessionID !== 'string') {
        return false;
      }
      return sessionId === req.sessionID && !shouldSave(req);
    }

    // determine if header should be set on response
    function shouldSetHeader(req: Request) {
      // cannot set header without a session ID
      if (typeof req.sessionID !== 'string') {
        return false;
      }
      if (alwaysSetHeader) {
        return true;
      }
      return sessionId != req.sessionID ? saveUninitializedSession || isModified(req.session) : rollingSessions && isModified(req.session);
    }

    // generate a session if the browser doesn't send a sessionID
    if (!req.sessionID) {
      generate();
      next();
      return;
    }

    // generate the session object
    store.get(req.sessionID, (err: Error & { code: string }, sess: Session) => {
      // error handling
      if (err) {
        if (err.code !== 'ENOENT') {
          next(err);
          return;
        }

        generate();
        // no session
      } else if (!sess) {
        generate();
        // populate req.session
      } else {
        store.createSession(req, sess);
        originalId = req.sessionID;
        originalHash = hash(sess);

        if (!resaveSession) {
          savedHash = originalHash
        }

        wrapmethods(req.session);
      }

      next();
    });
  };
}

/**
 * Generate a session ID for a new session.
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function generateSessionId(req?: Request) {
  return uid(24);
}

/**
 * Hash the given `sess` object omitting changes to `.cookie`.
 */
function hash(sess: Session) {
  return crypto.createHash('sha1').update(JSON.stringify(sess), 'utf8').digest('hex')
}

function setHeader(res: Response, name: string, val: string, secrets: string) {
  res.setHeader(name, `s:${signature.sign(val, secrets)}`);
}

function getHeader(req: Request, name: string, secret: string): string {
  const header: string = req.headers[name] as string;
  let val;
  // read from header
  if (header && header.substr(0, 2) === 's:') {
    val = signature.unsign(header.slice(2), secret);
    if (val === false) {
      val = undefined;
    }
  }
  return val;
}

cesco69 avatar Dec 30 '21 08:12 cesco69

This is the same as #161 right?

ultimate-tester avatar Feb 13 '22 14:02 ultimate-tester