diff --git a/README.md b/README.md index 0f0bafef..8d5ecf60 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Session data is _not_ saved in the cookie itself, just the session ID. #### Options - `name` - cookie name (formerly known as `key`). (default: `'connect.sid'`) + - `headerName` - optional HTTP header name to pass session ID, e.g. `X-Session-Token`. (default: `undefined`) - `store` - session store instance. - `secret` - session cookie is signed with this secret to prevent tampering. - `cookie` - session cookie settings. diff --git a/index.js b/index.js index 43d70e16..bf34c09b 100644 --- a/index.js +++ b/index.js @@ -85,6 +85,8 @@ function session(options){ var options = options || {} // name - previously "options.key" , name = options.name || options.key || 'connect.sid' + // optional name of HTTP header to pass session ID, e.g. 'X-Session-Token' + , headerName = options.header , store = options.store || new MemoryStore , cookie = options.cookie || {} , trustProxy = options.proxy @@ -92,6 +94,12 @@ function session(options){ , rollingSessions = options.rolling || false; var resaveSession = options.resave; var saveUninitializedSession = options.saveUninitialized; + var isCookieConfigurationSet = options.cookie !== null; + var headerNameNormalized; + if (headerName) { + // lower-case representation of header name to fetch header value from req.headers + headerNameNormalized = headerName.toLowerCase(); + } var generateId = options.genid || generateSessionId; @@ -126,7 +134,9 @@ function session(options){ store.generate = function(req){ req.sessionID = generateId(req); req.session = new Session(req); - req.session.cookie = new Cookie(cookie); + if (isCookieConfigurationSet) { + req.session.cookie = new Cookie(cookie); + } }; var storeImplementsTouch = typeof store.touch === 'function'; @@ -145,9 +155,11 @@ function session(options){ // the store has temporarily disconnected etc if (!storeReady) return debug('store is disconnected'), next(); - // pathname mismatch - var originalPath = parseUrl.original(req).pathname; - if (0 != originalPath.indexOf(cookie.path || '/')) return next(); + if (isCookieConfigurationSet) { + // pathname mismatch + var originalPath = parseUrl.original(req).pathname; + if (0 != originalPath.indexOf(cookie.path || '/')) return next(); + } // backwards compatibility for signed cookies // req.secret is passed from the cookie parser middleware @@ -163,8 +175,15 @@ function session(options){ // expose store req.sessionStore = store; - // get the session ID from the cookie - var cookieId = req.sessionID = getcookie(req, name, secret); + var cookieId; + if (isCookieConfigurationSet) { + // get the session ID from the cookie + cookieId = req.sessionID = getcookie(req, name, secret); + } + if (headerName) { + // get the session ID from the header + cookieId = req.sessionID = getHeader(req, headerNameNormalized, secret); + } // set-cookie onHeaders(res, function(){ @@ -173,19 +192,22 @@ function session(options){ return; } - var cookie = req.session.cookie; - - // only send secure cookies via https - if (cookie.secure && !issecure(req, trustProxy)) { - debug('not secured'); - return; + if (isCookieConfigurationSet) { + var cookie = req.session.cookie; + // only send secure cookies via https + if (cookie.secure && !issecure(req, trustProxy)) { + debug('not secured'); + return; + } + if (!shouldSetCookie(req)) { + return; + } + setcookie(res, name, req.sessionID, secret, cookie.data); } - if (!shouldSetCookie(req)) { - return; + if (headerName) { + setHeader(res, headerName, req.sessionID, secret); } - - setcookie(res, name, req.sessionID, secret, cookie.data); }); // proxy end() to commit the session @@ -573,3 +595,31 @@ function setcookie(res, name, val, secret, options) { res.setHeader('set-cookie', header) } + +function setHeader(res, name, val, secret) { + var signed = 's:' + signature.sign(val, secret); + debug(name + ' %s', signed); + + res.setHeader(name, signed); +} + +function getHeader(req, name, secret) { + var header = req.headers[name]; + 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') + } + } + + return val; +} diff --git a/session/session.js b/session/session.js index 4647f9da..8024f7e2 100644 --- a/session/session.js +++ b/session/session.js @@ -55,7 +55,9 @@ Session.prototype.touch = function(){ */ Session.prototype.resetMaxAge = function(){ - this.cookie.maxAge = this.cookie.originalMaxAge; + if (this.cookie) { + this.cookie.maxAge = this.cookie.originalMaxAge; + } return this; }; diff --git a/test/session.js b/test/session.js index b88e047a..01f2fd03 100644 --- a/test/session.js +++ b/test/session.js @@ -90,6 +90,38 @@ describe('session()', function(){ }) }) + it('should load session from header sid', function (done) { + var count = 0 + var header = 'X-Session-Token'; + var server = createServer({ header: header }, function (req, res) { + req.session.num = req.session.num || ++count + res.end('session ' + req.session.num) + }); + + request(server) + .get('/') + .expect(shouldHaveHeader(header)) + .expect(200, 'session 1', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set(header, sidHeader(res, header)) + .expect(200, 'session 1', done) + }) + }) + + it('should not respond with cookie if configuration cookie key set as null', function (done) { + var header = 'X-Session-Token'; + var server = createServer({ header: header, cookie: null }, function (req, res) { + res.end('session') + }); + + request(server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 'session', done) + }) + it('should pass session fetch error', function (done) { var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { @@ -1940,6 +1972,12 @@ function shouldNotHaveHeader(header) { } } +function shouldHaveHeader(header) { + return function (res) { + assert.ok(header.toLowerCase() in res.headers, 'should have ' + header + ' header') + } +} + function shouldSetCookie(name) { return function (res) { var header = cookie(res) @@ -1963,6 +2001,10 @@ function sid(res) { return val } +function sidHeader(res, name) { + return res.headers[name.toLowerCase()] +} + function writePatch() { var ended = false return function addWritePatch(req, res, next) {