client-sessions
Advanced tools
Comparing version 0.3.1 to 0.4.0
@@ -6,3 +6,3 @@ /* This Source Code Form is subject to the terms of the Mozilla Public | ||
var Cookies = require("cookies"); | ||
var Proxy = require("node-proxy"); | ||
var Proxy_ = (typeof Proxy !== 'undefined') ? Proxy : require("node-proxy"); | ||
var Handler = require("./ProxyHandler.js"); | ||
@@ -12,5 +12,6 @@ var crypto = require("crypto"); | ||
const COOKIE_NAME_SEP = '='; | ||
const ACTIVE_DURATION = 1000 * 60 * 5; | ||
function base64urlencode(arg) { | ||
var s = new Buffer(arg).toString('base64'); | ||
var s = arg.toString('base64'); | ||
s = s.split('=')[0]; // Remove any trailing '='s | ||
@@ -44,2 +45,14 @@ s = s.replace(/\+/g, '-'); // 62nd char of encoding | ||
function constantTimeEquals(a, b) { | ||
// Ideally this would be a native function, so it's less sensitive to how the | ||
// JS engine might optimize. | ||
if (a.length != b.length) | ||
return false; | ||
var ret = 0; | ||
for (var i = 0; i < a.length; i++) { | ||
ret |= a.readUInt8(i) ^ b.readUInt8(i); | ||
} | ||
return ret == 0; | ||
} | ||
function encode(opts, content, duration, createdAt){ | ||
@@ -74,2 +87,4 @@ // format will be: | ||
ciphertext += cipher.final('binary'); | ||
// Before 0.10, crypto returns binary-encoded strings. Remove when | ||
// dropping 0.8 support. | ||
ciphertext = new Buffer(ciphertext, 'binary'); | ||
@@ -88,2 +103,5 @@ | ||
var hmac = hmacAlg.digest(); | ||
// Before 0.10, crypto returns binary-encoded strings. Remove when | ||
// dropping 0.8 support. | ||
hmac = new Buffer(hmac, 'binary'); | ||
@@ -133,4 +151,7 @@ return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + createdAt + "." + duration + "." + base64urlencode(hmac); | ||
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'); | ||
if (hmac.toString('utf8') != expected_hmac.toString('utf8')) | ||
if (!constantTimeEquals(hmac, expected_hmac)) | ||
return; | ||
@@ -169,5 +190,4 @@ | ||
this.opts = opts; | ||
// support for maxAge | ||
if (opts.cookie.maxAge) { | ||
this.expires = new Date(new Date().getTime() + opts.cookie.maxAge); | ||
if (opts.cookie.ephemeral && opts.cookie.maxAge) { | ||
throw new Error("you cannot have an ephemeral cookie with a maxAge."); | ||
} | ||
@@ -183,3 +203,11 @@ | ||
this.duration = opts.duration; | ||
this.activeDuration = opts.activeDuration; | ||
// support for maxAge | ||
if (opts.cookie.maxAge) { | ||
this.expires = new Date(new Date().getTime() + opts.cookie.maxAge); | ||
} else { | ||
this.updateDefaultExpires(); | ||
} | ||
// here, we check that the security bits are set correctly | ||
@@ -192,2 +220,15 @@ var secure = res.socket.encrypted || req.connection.proxySecure; | ||
Session.prototype = { | ||
updateDefaultExpires: function() { | ||
if (this.opts.cookie.maxAge) return; | ||
if (this.opts.cookie.ephemeral) { | ||
this.expires = null; | ||
} else { | ||
var time = this.createdAt || new Date().getTime(); | ||
// the cookie should expire when it becomes invalid | ||
// we add an extra second because the conversion to a date truncates the milliseconds | ||
this.expires = new Date(time + this.duration + 1000); | ||
} | ||
}, | ||
clearContent: function(keysToPreserve) { | ||
@@ -208,2 +249,3 @@ var self = this; | ||
this.duration = this.opts.duration; | ||
this.updateDefaultExpires(); | ||
this.dirty = true; | ||
@@ -213,3 +255,6 @@ this.loaded = true; | ||
setDuration: function(newDuration) { | ||
setDuration: function(newDuration, ephemeral) { | ||
if (ephemeral && this.opts.cookie.maxAge) { | ||
throw new Error("you cannot have an ephemeral cookie with a maxAge."); | ||
} | ||
if (!this.loaded) | ||
@@ -220,2 +265,4 @@ this.loadFromCookie(true); | ||
this.createdAt = new Date().getTime(); | ||
this.opts.cookie.ephemeral = ephemeral; | ||
this.updateDefaultExpires(); | ||
}, | ||
@@ -243,2 +290,3 @@ | ||
this.duration = unboxed.duration; | ||
this.updateDefaultExpires(); | ||
}, | ||
@@ -248,6 +296,4 @@ | ||
if (this.dirty) { | ||
// support for expires | ||
if (this.expires) { | ||
this.opts.cookie.expires = this.expires; | ||
} | ||
// support for adding/removing cookie expires | ||
this.opts.cookie.expires = this.expires; | ||
@@ -268,5 +314,13 @@ try { | ||
var expiresAt = this.createdAt + this.duration; | ||
var now = Date.now(); | ||
// should we reset this session? | ||
if ((this.createdAt + this.duration) < new Date().getTime()) | ||
if (expiresAt < now) | ||
this.reset(); | ||
// if expiration is soon, push back a few minutes to not interrupt user | ||
else if (expiresAt - now < this.activeDuration) { | ||
this.createdAt += this.activeDuration; | ||
this.dirty = true; | ||
this.updateDefaultExpires(); | ||
} | ||
} else { | ||
@@ -331,3 +385,3 @@ if (force_reset) { | ||
var proxySession = Proxy.create(sessionHandler); | ||
var proxySession = Proxy_.create(sessionHandler); | ||
return proxySession; | ||
@@ -349,2 +403,3 @@ } | ||
opts.duration = opts.duration || 24*60*60*1000; | ||
opts.activeDuration = 'activeDuration' in opts ? opts.activeDuration : ACTIVE_DURATION; | ||
@@ -368,3 +423,9 @@ // set up cookie defaults | ||
const propertyName = opts.requestKey || opts.cookieName; | ||
return function(req, res, next) { | ||
if (req[propertyName]) { | ||
return next(); //self aware | ||
} | ||
var cookies = new Cookies(req, res); | ||
@@ -380,3 +441,3 @@ var raw_session; | ||
req[opts.requestKey || opts.cookieName] = raw_session.monitor(); | ||
req[propertyName] = raw_session.monitor(); | ||
@@ -383,0 +444,0 @@ res.on('header', function() { |
@@ -0,0 +0,0 @@ |
{ | ||
"name" : "client-sessions", | ||
"version" : "0.3.1", | ||
"private" : false, | ||
"version" : "0.4.0", | ||
"description" : "secure sessions stored in cookies", | ||
@@ -12,7 +11,7 @@ "main" : "lib/client-sessions", | ||
"dependencies" : { | ||
"cookies" : "0.2.1", | ||
"cookies" : "0.3.6", | ||
"node-proxy": "0.6.0" | ||
}, | ||
"devDependencies": { | ||
"vows": "0.5.13", | ||
"vows": "0.7.0", | ||
"express": "2.5.0", | ||
@@ -31,3 +30,8 @@ "tobi": "https://github.com/Cowboy-coder/tobi/tarball/fd733a3", | ||
"node": ">= 0.8.0" | ||
} | ||
}, | ||
"licenses": { | ||
"type": "MPL 2.0", | ||
"url": "https://raw.github.com/mozilla/node-client-sessions/master/LICENSE" | ||
}, | ||
"bugs": "https://github.com/mozilla/node-client-sessions/issues" | ||
} |
@@ -18,2 +18,3 @@ [![build status](https://secure.travis-ci.org/mozilla/node-client-sessions.png)](http://travis-ci.org/mozilla/node-client-sessions) | ||
duration: 24 * 60 * 60 * 1000, // how long the session will stay valid in ms | ||
activeDuration: 1000 * 60 * 5 // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds | ||
})); | ||
@@ -40,2 +41,4 @@ | ||
path: '/api', // cookie will only be sent to requests under '/api' | ||
maxAge: 60000, // duration of the cookie in milliseconds, defaults to duration above | ||
ephemeral: false, // when true, cookie expires when the browser closes | ||
httpOnly: true, // when true, cookie is not accessible from javascript | ||
@@ -46,3 +49,3 @@ secure: false // when true, cookie will only be sent over SSL | ||
Finally, you can have multiple cookies: | ||
You can have multiple cookies: | ||
@@ -65,2 +68,22 @@ // a 1 week session | ||
Finally, you can use requestKey to force the name where information can be accessed on the request object. | ||
var sessions = require("client-sessions"); | ||
app.use(sessions({ | ||
cookieName: 'mySession', | ||
requestKey: 'forcedSessionKey', // requestKey overrides cookieName for the key name added to the request object. | ||
secret: 'blargadeeblargblarg', // should be a large unguessable string | ||
duration: 24 * 60 * 60 * 1000, // how long the session will stay valid in ms | ||
})); | ||
app.use(function(req, res, next) { | ||
// requestKey forces the session information to be | ||
// accessed via forcedSessionKey | ||
if (req.forcedSessionKey.seenyou) { | ||
res.setHeader('X-Seen-You', 'true'); | ||
} | ||
next(); | ||
}); | ||
## License | ||
@@ -67,0 +90,0 @@ |
@@ -19,2 +19,3 @@ // a NODE_ENV of test will supress console output to stderr which | ||
secret: 'yo', | ||
activeDuration: 0, | ||
cookie: { | ||
@@ -32,2 +33,3 @@ maxAge: 5000 | ||
secret: 'yo', | ||
activeDuration: 0, | ||
cookie: { | ||
@@ -334,2 +336,3 @@ maxAge: 5000 | ||
secret: 'yo', | ||
activeDuration: 0, | ||
duration: 500 // 0.5 seconds | ||
@@ -422,3 +425,3 @@ })); | ||
suite.addBatch({ | ||
"querying twice, each at 3/4 duration time": { | ||
"querying twice, each at 2/5 duration time": { | ||
topic: function() { | ||
@@ -445,2 +448,39 @@ var self = this; | ||
setTimeout(function () { | ||
// so the session should still be valid | ||
browser.get("/bar2", function(res, $) { | ||
}); | ||
}, 200); | ||
}); | ||
}, 200); | ||
}); | ||
}, | ||
"session still has state": function(err, req) { | ||
assert.isDefined(req.session.baz); | ||
} | ||
} | ||
}); | ||
suite.addBatch({ | ||
"querying twice, each at 3/5 duration time": { | ||
topic: function() { | ||
var self = this; | ||
var app = create_app_with_duration(); | ||
app.get("/bar", function(req, res) { | ||
req.session.baz = Math.random(); | ||
res.send("bar"); | ||
}); | ||
app.get("/bar2", function(req, res) { | ||
self.callback(null, req); | ||
res.send("bar2"); | ||
}); | ||
var browser = tobi.createBrowser(app); | ||
// first query resets the session to full duration | ||
browser.get("/foo", function(res, $) { | ||
setTimeout(function () { | ||
// this query should NOT reset the session | ||
browser.get("/bar", function(res, $) { | ||
setTimeout(function () { | ||
// so the session should be dead by now | ||
@@ -467,2 +507,3 @@ browser.get("/bar2", function(res, $) { | ||
secret: 'yobaby', | ||
activeDuration: 0, | ||
duration: 5000 // 5.0 seconds | ||
@@ -651,2 +692,3 @@ })); | ||
secret: 'yo', | ||
activeDuration: 0, | ||
cookie: { | ||
@@ -760,2 +802,11 @@ maxAge: 5000, | ||
assert.equal(decoded.duration, 86400000); | ||
}, | ||
"encode and decode - tampered HMAC" : function(err, req){ | ||
var encodedReal = 'LVB3G2lnPF75RzsT9mz7jQ.RT1Lcq0dOJ_DMRHyWJ4NZPjBXr2WzkFcUC4NO78gbCQ.1371704898483.5000.ILEusgnajT1sqCWLuzaUt-HFn2KPjYNd38DhI7aRCb9'; | ||
var encodedFake = encodedReal.substring(0, encodedReal.length - 1) + 'A'; | ||
var decodedReal = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encodedReal); | ||
assert.isObject(decodedReal); | ||
var decodedFake = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encodedFake); | ||
assert.isUndefined(decodedFake); | ||
} | ||
@@ -799,2 +850,3 @@ } | ||
cookieName: 'ooga_booga_momma', | ||
activeDuration: 0, | ||
requestKey: 'ses', | ||
@@ -805,3 +857,3 @@ secret: 'yo' | ||
app.get('/foo', function(req, res) { | ||
self.callback(null, req) | ||
self.callback(null, req); | ||
}); | ||
@@ -871,2 +923,216 @@ | ||
suite.addBatch({ | ||
"missing cookie maxAge": { | ||
topic: function() { | ||
var self = this; | ||
var app = express.createServer(); | ||
app.use(cookieSessions({ | ||
cookieName: 'session', | ||
duration: 50000, | ||
activeDuration: 0, | ||
secret: 'yo' | ||
})); | ||
app.get("/foo", function(req, res) { | ||
req.session.foo = 'foobar'; | ||
res.send("hello"); | ||
}); | ||
var browser = tobi.createBrowser(app); | ||
browser.get("/foo", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, | ||
"still has an expires attribute": function(err, res) { | ||
assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie"); | ||
}, | ||
"which roughly matches the session duration": function(err, res) { | ||
var expiryValue = res.headers['set-cookie'][0].replace(/^.*expires=([^;]+);.*$/, "$1"); | ||
var expiryDate = new Date(expiryValue); | ||
var cookieDuration = expiryDate.getTime() - Date.now(); | ||
assert(Math.abs(50000 - cookieDuration) < 1500, "expiry is pretty far from the specified duration"); | ||
} | ||
}, | ||
"changing the duration": { | ||
topic: function() { | ||
var self = this; | ||
var app = express.createServer(); | ||
app.use(cookieSessions({ | ||
cookieName: 'session', | ||
duration: 500, | ||
activeDuration: 0, | ||
secret: 'yo' | ||
})); | ||
app.get("/foo", function(req, res) { | ||
req.session.foo = 'foobar'; | ||
res.send("hello"); | ||
}); | ||
app.get("/bar", function(req, res) { | ||
req.session.setDuration(5000); | ||
res.send("bar"); | ||
}); | ||
var browser = tobi.createBrowser(app); | ||
browser.get("/foo", function(res, $) { | ||
setTimeout(function () { | ||
browser.get("/bar", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, 200); | ||
}); | ||
}, | ||
"updates the cookie expiry": function(err, res) { | ||
var expiryValue = res.headers['set-cookie'][0].replace(/^.*expires=([^;]+);.*$/, "$1"); | ||
var expiryDate = new Date(expiryValue); | ||
var cookieDuration = expiryDate.getTime() - Date.now(); | ||
assert(Math.abs(cookieDuration - 5000) < 1000, "expiry is pretty far from the specified duration"); | ||
} | ||
}, | ||
"active user with session close to expiration": { | ||
topic: function() { | ||
var app = express.createServer(); | ||
var self = this; | ||
app.use(cookieSessions({ | ||
cookieName: 'session', | ||
duration: 300, | ||
activeDuration: 500, | ||
secret: 'yo' | ||
})); | ||
app.get("/foo", function(req, res) { | ||
req.session.foo = 'foobar'; | ||
res.send("hello"); | ||
}); | ||
app.get("/bar", function(req, res) { | ||
req.session.bar = 'baz'; | ||
res.send('hi'); | ||
}); | ||
app.get("/baz", function(req, res) { | ||
res.json({ "msg": req.session.foo + req.session.bar }); | ||
}); | ||
var browser = tobi.createBrowser(app); | ||
browser.get("/foo", function() { | ||
browser.get("/bar", function() { | ||
setTimeout(function () { | ||
browser.get("/baz", function(res, first) { | ||
setTimeout(function() { | ||
browser.get('/baz', function(res, second) { | ||
self.callback(null, first, second); | ||
}); | ||
}, 1000); | ||
}); | ||
}, 400); | ||
}); | ||
}); | ||
}, | ||
"extends session duration": function(err, extended, tooLate) { | ||
assert.equal(extended.msg, 'foobarbaz'); | ||
assert.equal(tooLate.msg, null); | ||
} | ||
} | ||
}); | ||
var shared_browser1; | ||
var shared_browser2; | ||
suite.addBatch({ | ||
"non-ephemeral cookie": { | ||
topic: function() { | ||
var self = this; | ||
var app = express.createServer(); | ||
app.use(cookieSessions({ | ||
cookieName: 'session', | ||
duration: 5000, | ||
secret: 'yo', | ||
cookie: { | ||
ephemeral: false | ||
} | ||
})); | ||
app.get("/foo", function(req, res) { | ||
req.session.foo = 'foobar'; | ||
res.send("hello"); | ||
}); | ||
app.get("/bar", function(req, res) { | ||
req.session.setDuration(6000, true); | ||
res.send("hello"); | ||
}); | ||
shared_browser1 = tobi.createBrowser(app); | ||
shared_browser1.get("/foo", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, | ||
"has an expires attribute": function(err, res) { | ||
assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie"); | ||
}, | ||
"changing to an ephemeral one": { | ||
topic: function() { | ||
var self = this; | ||
shared_browser1.get("/bar", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, | ||
"removes its expires attribute": function(err, res) { | ||
assert.strictEqual(res.headers['set-cookie'][0].indexOf('expires='), -1, "cookie is not ephemeral"); | ||
} | ||
} | ||
}, | ||
"ephemeral cookie": { | ||
topic: function() { | ||
var self = this; | ||
var app = express.createServer(); | ||
app.use(cookieSessions({ | ||
cookieName: 'session', | ||
duration: 50000, | ||
activeDuration: 0, | ||
secret: 'yo', | ||
cookie: { | ||
ephemeral: true | ||
} | ||
})); | ||
app.get("/foo", function(req, res) { | ||
req.session.foo = 'foobar'; | ||
res.send("hello"); | ||
}); | ||
app.get("/bar", function(req, res) { | ||
req.session.setDuration(6000, false); | ||
res.send("hello"); | ||
}); | ||
shared_browser2 = tobi.createBrowser(app); | ||
shared_browser2.get("/foo", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, | ||
"doesn't have an expires attribute": function(err, res) { | ||
assert.strictEqual(res.headers['set-cookie'][0].indexOf('expires='), -1, "cookie is not ephemeral"); | ||
}, | ||
"changing to an non-ephemeral one": { | ||
topic: function() { | ||
var self = this; | ||
shared_browser2.get("/bar", function(res, $) { | ||
self.callback(null, res); | ||
}); | ||
}, | ||
"gains an expires attribute": function(err, res) { | ||
assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie"); | ||
} | ||
} | ||
} | ||
}); | ||
suite.export(module); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Nonpermissive License
License(Experimental) A package's licensing information has fine-grained problems
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
69415
1428
90
1
+ Addedcookies@0.3.6(transitive)
- Removedcookies@0.2.1(transitive)
Updatedcookies@0.3.6