Socket
Socket
Sign inDemoInstall

client-sessions

Package Overview
Dependencies
1
Maintainers
3
Versions
19
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.4.1 to 0.5.0

.jshintrc

400

lib/client-sessions.js

@@ -5,10 +5,15 @@ /* This Source Code Form is subject to the terms of the Mozilla Public

var Cookies = require("cookies");
var Proxy_ = (typeof Proxy !== 'undefined') ? Proxy : require("node-proxy");
var Handler = require("./ProxyHandler.js");
var crypto = require("crypto");
const Cookies = require("cookies");
const crypto = require("crypto");
const util = require("util");
const COOKIE_NAME_SEP = '=';
const ACTIVE_DURATION = 1000 * 60 * 5;
function isObject(val) {
return Object.prototype.toString.call(val) === '[object Object]';
}
function base64urlencode(arg) {

@@ -27,8 +32,13 @@ var s = arg.toString('base64');

s = s.replace(/_/g, '/'); // 63rd char of encoding
switch (s.length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: s += "=="; break; // Two pad chars
case 3: s += "="; break; // One pad char
default: throw new Error("Illegal base64url string!");
switch (s.length % 4) { // Pad with trailing '='s
case 0:
break; // No pad chars in this case
case 2:
s += "==";
break; // Two pad chars
case 3:
s += "=";
break; // One pad char
default:
throw new Error("Illegal base64url string!");
}

@@ -48,4 +58,5 @@ return new Buffer(s, 'base64'); // Standard base64 decoder

// JS engine might optimize.
if (a.length != b.length)
if (a.length !== b.length) {
return false;
}
var ret = 0;

@@ -55,54 +66,60 @@ for (var i = 0; i < a.length; i++) {

}
return ret == 0;
return ret === 0;
}
function encode(opts, content, duration, createdAt){
// format will be:
// iv.ciphertext.createdAt.duration.hmac
// format will be:
// iv.ciphertext.createdAt.duration.hmac
if (!opts.cookieName) {
throw new Error('cookieName option required');
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) != -1) {
throw new Error('cookieName cannot include "="');
}
if (!opts.cookieName) {
throw new Error('cookieName option required');
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) !== -1) {
throw new Error('cookieName cannot include "="');
}
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.signatureKey) {
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
}
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
}
duration = duration || 24*60*60*1000;
createdAt = createdAt || new Date().getTime();
duration = duration || 24*60*60*1000;
createdAt = createdAt || new Date().getTime();
// generate iv
var iv = crypto.randomBytes(16);
// generate iv
var iv = crypto.randomBytes(16);
// encrypt with encryption key
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
ciphertext += cipher.final('binary');
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
ciphertext = new Buffer(ciphertext, 'binary');
// encrypt with encryption key
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
ciphertext += cipher.final('binary');
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
ciphertext = new Buffer(ciphertext, 'binary');
// hmac it
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(duration.toString());
// hmac it
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(duration.toString());
var hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
hmac = new Buffer(hmac, 'binary');
var hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
hmac = new Buffer(hmac, 'binary');
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + createdAt + "." + duration + "." + base64urlencode(hmac);
return [
base64urlencode(iv),
base64urlencode(ciphertext),
createdAt,
duration,
base64urlencode(hmac)
].join('.');
}

@@ -112,66 +129,71 @@

// stop at any time if there's an issue
var components = content.split(".");
if (components.length != 5)
return;
// stop at any time if there's an issue
var components = content.split(".");
if (components.length !== 5) {
return;
}
if (!opts.cookieName) {
throw new Error("cookieName option required");
}
if (!opts.cookieName) {
throw new Error("cookieName option required");
}
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.signatureKey) {
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
}
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
}
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var createdAt = parseInt(components[2], 10);
var duration = parseInt(components[3], 10);
var hmac = base64urldecode(components[4]);
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var createdAt = parseInt(components[2], 10);
var duration = parseInt(components[3], 10);
var hmac = base64urldecode(components[4]);
// make sure IV is right length
if (iv.length != 16)
return;
// make sure IV is right length
if (iv.length !== 16) {
return;
}
// check hmac
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(duration.toString());
// check hmac
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(duration.toString());
var expected_hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
expected_hmac = new Buffer(expected_hmac, 'binary');
var expectedHmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
expectedHmac = new Buffer(expectedHmac, 'binary');
if (!constantTimeEquals(hmac, expected_hmac))
return;
if (!constantTimeEquals(hmac, expectedHmac)) {
return;
}
// decrypt
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
plaintext += cipher.final('utf8');
// decrypt
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
plaintext += cipher.final('utf8');
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
if (cookieName !== opts.cookieName) {
return;
}
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
if (cookieName !== opts.cookieName) {
return;
}
try {
return {
content: JSON.parse(plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)),
createdAt: createdAt,
duration: duration
};
} catch (x) {
return;
}
try {
return {
content: JSON.parse(
plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)
),
createdAt: createdAt,
duration: duration
};
} catch (x) {
return;
}
}

@@ -194,2 +216,3 @@

this.content = {};
this.json = JSON.stringify(this._content);
this.loaded = false;

@@ -212,5 +235,8 @@ this.dirty = false;

// here, we check that the security bits are set correctly
var secure = (res.socket && res.socket.encrypted) || (req.connection && req.connection.proxySecure);
if (opts.cookie.secure && !secure)
throw new Error("you cannot have a secure cookie unless the socket is secure or you declare req.connection.proxySecure to be true.");
var secure = (res.socket && res.socket.encrypted) ||
(req.connection && req.connection.proxySecure);
if (opts.cookie.secure && !secure) {
throw new Error("you cannot have a secure cookie unless the socket is " +
" secure or you declare req.connection.proxySecure to be true.");
}
}

@@ -220,3 +246,5 @@

updateDefaultExpires: function() {
if (this.opts.cookie.maxAge) return;
if (this.opts.cookie.maxAge) {
return;
}

@@ -228,3 +256,4 @@ if (this.opts.cookie.ephemeral) {

// the cookie should expire when it becomes invalid
// we add an extra second because the conversion to a date truncates the milliseconds
// we add an extra second because the conversion to a date
// truncates the milliseconds
this.expires = new Date(time + this.duration + 1000);

@@ -236,8 +265,9 @@ }

var self = this;
Object.keys(this.content).forEach(function(k) {
Object.keys(this._content).forEach(function(k) {
// exclude this key if it's meant to be preserved
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1))
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1)) {
return;
}
delete self.content[k];
delete self._content[k];
});

@@ -259,4 +289,5 @@ },

}
if (!this.loaded)
if (!this.loaded) {
this.loadFromCookie(true);
}
this.dirty = true;

@@ -272,3 +303,3 @@ this.duration = newDuration;

box: function() {
return encode(this.opts, this.content, this.duration, this.createdAt);
return encode(this.opts, this._content, this.duration, this.createdAt);
},

@@ -280,8 +311,11 @@

var unboxed = decode(this.opts, content);
if (!unboxed) return;
if (!unboxed) {
return;
}
var self = this;
Object.keys(unboxed.content).forEach(function(k) {
self.content[k] = unboxed.content[k];
self._content[k] = unboxed.content[k];
});

@@ -295,3 +329,3 @@

updateCookie: function() {
if (this.dirty) {
if (this.isDirty()) {
// support for adding/removing cookie expires

@@ -309,3 +343,3 @@ this.opts.cookie.expires = this.expires;

loadFromCookie: function(force_reset) {
loadFromCookie: function(forceReset) {
var cookie = this.cookies.get(this.opts.cookieName);

@@ -318,6 +352,6 @@ if (cookie) {

// should we reset this session?
if (expiresAt < now)
if (expiresAt < now) {
this.reset();
// if expiration is soon, push back a few minutes to not interrupt user
else if (expiresAt - now < this.activeDuration) {
} else if (expiresAt - now < this.activeDuration) {
this.createdAt += this.activeDuration;

@@ -328,3 +362,3 @@ this.dirty = true;

} else {
if (force_reset) {
if (forceReset) {
this.reset();

@@ -337,75 +371,53 @@ } else {

this.loaded = true;
this.json = JSON.stringify(this._content);
return true;
},
// called to create a proxy that monitors the session
// for new properties being set
monitor: function() {
// the target for the proxy will be the content
// variable, to simplify the proxying
var sessionHandler = new Handler(this.content);
var raw_session = this;
isDirty: function() {
return this.dirty || (this.json !== JSON.stringify(this._content));
}
// all values from content except special values
sessionHandler.get = function(rcvr, name) {
if (['reset'].indexOf(name) > -1) {
return raw_session[name].bind(raw_session);
} else if (['setDuration'].indexOf(name) > -1) {
return raw_session[name].bind(raw_session);
} else {
if (!raw_session.loaded) {
var didLoad = raw_session.loadFromCookie();
if (!didLoad) return undefined;
}
return this.target[name];
}
};
};
// set all values to content
sessionHandler.set = function(rcvr, name, value) {
// we have to load existing content, otherwise it will later override
// the content that is written.
if (!raw_session.loaded)
raw_session.loadFromCookie(true);
Object.defineProperty(Session.prototype, 'content', {
get: function getContent() {
if (!this.loaded) {
this.loadFromCookie();
}
return this._content;
},
set: function setContent(value) {
Object.defineProperty(value, 'reset', {
enumerable: false,
value: this.reset.bind(this)
});
Object.defineProperty(value, 'setDuration', {
enumerable: false,
value: this.setDuration.bind(this)
});
this._content = value;
}
});
this.target[name] = value;
raw_session.dirty = true;
};
// if key is deleted
sessionHandler.delete = function(name) {
// we have to load existing content, otherwise it will later override
// the content that is written.
if (!raw_session.loaded) {
var didLoad = raw_session.loadFromCookie();
if (!didLoad) return;
}
function clientSessionFactory(opts) {
if (!opts) {
throw new Error("no options provided, some are required");
}
delete this.target[name];
raw_session.dirty = true;
};
var proxySession = Proxy_.create(sessionHandler);
return proxySession;
if (!opts.secret) {
throw new Error("cannot set up sessions without a secret");
}
};
var cookieSession = function(opts) {
if (!opts)
throw "no options provided, some are required"; // XXX rename opts?
if (!opts.secret)
throw "cannot set up sessions without a secret";
// defaults
opts.cookieName = opts.cookieName || "session_state";
opts.duration = opts.duration || 24*60*60*1000;
opts.activeDuration = 'activeDuration' in opts ? opts.activeDuration : ACTIVE_DURATION;
opts.activeDuration = 'activeDuration' in opts ?
opts.activeDuration : ACTIVE_DURATION;
// set up cookie defaults
opts.cookie = opts.cookie || {};
if (typeof(opts.cookie.httpOnly) == 'undefined')
if (typeof opts.cookie.httpOnly === 'undefined') {
opts.cookie.httpOnly = true;
}

@@ -426,4 +438,4 @@ // let's not default to secure just yet,

return function(req, res, next) {
if (req[propertyName]) {
return function clientSession(req, res, next) {
if (propertyName in req) {
return next(); //self aware

@@ -433,24 +445,38 @@ }

var cookies = new Cookies(req, res);
var raw_session;
var rawSession;
try {
raw_session = new Session(req, res, cookies, opts);
rawSession = new Session(req, res, cookies, opts);
} catch (x) {
// this happens only if there's a big problem
process.nextTick(function() {next("client-sessions error: " + x.toString());});
process.nextTick(function() {
next("client-sessions error: " + x.toString());
});
return;
}
req[propertyName] = raw_session.monitor();
Object.defineProperty(req, propertyName, {
get: function getSession() {
return rawSession.content;
},
set: function setSession(value) {
if (isObject(value)) {
rawSession.content = value;
} else {
throw new TypeError("cannot set client-session to non-object");
}
}
});
var writeHead = res.writeHead;
res.writeHead = function () {
raw_session.updateCookie();
rawSession.updateCookie();
return writeHead.apply(res, arguments);
}
};
next();
};
};
}
module.exports = cookieSession;
module.exports = clientSessionFactory;

@@ -457,0 +483,0 @@

{
"name" : "client-sessions",
"version" : "0.4.1",
"version" : "0.5.0",
"description" : "secure sessions stored in cookies",

@@ -11,4 +11,3 @@ "main" : "lib/client-sessions",

"dependencies" : {
"cookies" : "0.3.6",
"node-proxy": "0.6.0"
"cookies" : "0.3.8"
},

@@ -15,0 +14,0 @@ "devDependencies": {

@@ -46,14 +46,22 @@ // a NODE_ENV of test will supress console output to stderr which

suite.addBatch({
"a single request object" : {
"middleware" : {
topic: function() {
var self = this;
var middleware = cookieSessions({
cookieName: 'session',
secret: 'yo',
activeDuration: 0,
cookie: {
maxAge: 5000
}
});
var app = create_app();
app.get("/foo", function(req, res) {
self.callback(null, req);
res.send("hello");
var req = {
headers: {}
};
var res = {};
middleware(req, res, function(err) {
self.callback(err, req, res);
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {});
},

@@ -70,2 +78,5 @@ "includes a session object": function(err, req) {

},
"session object has setDuration function": function(err, req) {
assert.isFunction(req.session.setDuration);
},
"set variables and clear them yields no variables": function(err, req) {

@@ -91,2 +102,17 @@ req.session.bar = 'baz';

assert.equal(req.session.foo, 'foobar');
},
"set session property absorbs set object": function(err, req) {
req.session.reset();
req.session.foo = 'quux';
req.session = { bar: 'baz' };
assert.isUndefined(req.session.foo);
assert.isFunction(req.session.reset);
assert.isFunction(req.session.setDuration);
assert.equal(req.session.bar, 'baz');
assert.throws(function() {
req.session = 'blah';
}, TypeError);
}

@@ -237,2 +263,39 @@ }

}
},
"across three requests with deep objects" : {
topic: function() {
var self = this;
// simple app
var app = create_app();
app.get("/foo", function(req, res) {
req.session.reset();
req.session.foo = 'foobar';
req.session.bar = { a: 'b' };
res.send("foo");
});
app.get("/bar", function(req, res) {
req.session.bar.c = 'd';
res.send("bar");
});
app.get("/baz", function(req, res) {
self.callback(null, req);
res.send("baz");
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
browser.get("/bar", function(res, $) {
browser.get("/baz", function(res, $) {
});
});
});
},
"session maintains state": function(err, req) {
assert.equal(req.session.foo, 'foobar');
assert.equal(req.session.bar.c, 'd');
}
}

@@ -239,0 +302,0 @@ });

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc