-
Notifications
You must be signed in to change notification settings - Fork 41
/
session.js
144 lines (124 loc) · 4.84 KB
/
session.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var mach = require('../index');
var Promise = require('../utils/Promise');
var decodeBase64 = require('../utils/decodeBase64');
var encodeBase64 = require('../utils/encodeBase64');
var makeHash = require('../utils/makeHash');
var CookieStore = require('./session/CookieStore');
mach.extend(
require('../extensions/server')
);
/**
* The maximum size of an HTTP cookie.
*/
var MAX_COOKIE_SIZE = 4096;
/**
* Stores the given session and returns a promise for a value that should be stored
* in the session cookie to retrieve the session data again on the next request.
*/
function encodeSession(session, store, secret) {
return store.save(session).then(function (data) {
var cookie = encodeBase64(data + '--' + makeHashWithSecret(data, secret));
if (cookie.length > MAX_COOKIE_SIZE)
throw new Error('Cookie data size exceeds 4kb; content dropped');
return cookie;
});
}
/**
* Decodes the given cookie value and returns a promise for the corresponding session
* data from the store. Also verifies the hash value to ensure the cookie has not been
* tampered with. If it has, returns null.
*/
function decodeCookie(cookie, store, secret) {
var value = decodeBase64(cookie);
var index = value.lastIndexOf('--');
var data = value.substring(0, index);
var hash = value.substring(index + 2);
// Verify the cookie has not been tampered with.
if (hash === makeHashWithSecret(data, secret))
return store.load(data);
return null;
}
function makeHashWithSecret(data, secret) {
return makeHash(secret ? data + secret : data);
}
/**
* A middleware that provides support for HTTP sessions using cookies.
*
* Options may be any of the following:
*
* - secret A cryptographically secure secret key that will be used to verify
* the integrity of session data that is received from the client
* - name The name of the cookie. Defaults to "_session"
* - path The path of the cookie. Defaults to "/"
* - domain The cookie's domain. Defaults to null
* - secure True to only send this cookie over HTTPS. Defaults to false
* - expireAfter The number of seconds after which sessions expire. Defaults
* to 0 (no expiration)
* - httpOnly True to restrict access to this cookie to HTTP(S) APIs.
* Defaults to true
* - store An instance of MemoryStore, CookieStore, or RedisStore that
* is used to store session data. Defaults to a new CookieStore
*
* Example:
*
* app.use(mach.session, {
* secret: 'the-secret',
* secure: true
* });
*
* Hint: A great way to generate a cryptographically secure session secret from
* the command line:
*
* $ node -p "require('crypto').randomBytes(64).toString('hex')"
*
* Note: Since cookies are only able to reliably store about 4k of data, if the
* session cookie payload exceeds that the session will be dropped.
*/
function session(app, options) {
options = options || {};
if (typeof options === 'string')
options = { secret: options };
var secret = options.secret;
var name = options.name || '_session';
var path = options.path || '/';
var domain = options.domain;
var expireAfter = options.expireAfter || 0;
var httpOnly = ('httpOnly' in options) ? (options.httpOnly || false) : true;
var secure = options.secure || false;
var store = options.store || new CookieStore(options);
if (!secret) {
console.warn([
'WARNING: There was no "secret" option provided to mach.session! This poses',
'a security vulnerability because session data will be stored on clients without',
'any server-side verification that it has not been tampered with. It is strongly',
'recommended that you set a secret to prevent exploits that may be attempted using',
'carefully crafted cookies.'
].join('\n'));
}
return function (conn) {
if (conn.session)
return conn.call(app); // Don't overwrite the existing session.
var cookie = conn.request.cookies[name];
return Promise.resolve(cookie && decodeCookie(cookie, store, secret)).then(function (object) {
conn.session = object || {};
return conn.call(app).then(function () {
return Promise.resolve(conn.session && encodeSession(conn.session, store, secret)).then(function (newCookie) {
var expires = expireAfter && new Date(Date.now() + (expireAfter * 1000));
// Don't bother setting the cookie if its value
// hasn't changed and there is no expires date.
if (newCookie === cookie && !expires)
return;
conn.response.setCookie(name, {
value: newCookie,
path: path,
domain: domain,
expires: expires,
httpOnly: httpOnly,
secure: secure
});
}, conn.onError);
});
}, conn.onError);
};
}
module.exports = session;