session
session copied to clipboard
Cookieless Session
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?
You could just call the get and set methods on your store object directly.
can you give me an example for that? How I'm supposed to do that?
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 , 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?
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:
- Version of Node.js
- Version of this module
- Complete server code that reproduces the issue
- Instructions on how to get the server running if it is more than copy and paste your provided code
- Instructions on how to reproduce in tge browser. Usually (1) go to this address (2) click this link, etc.
Thanks!
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 -
- 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 }) }));
-
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".
-
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.
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.
gateway.zip here's the code.
Awesome, thanks! I just got to bed, so will check it out when off work tomorrow night (approx in 20 hours).
(I also have the issue open and labeled in case someone else has time to look sooner)
@dougwilson thanks for your help.
Hi @dougwilson, any update?
I haven't had an opportunity yet. I have the issue open and labeled in case someone else has time to look sooner.
It's been a while! What happened?
@npshubh I am able to understand the problem,I wanted you to clarify the following
-
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.
-
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.
-
Can please provide me minimal code to reproduce this issue.
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.
- 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)?
- Is there any other known way of putting the cookie in the headers?
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.
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
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;
}
This is the same as #161 right?