nodebb-plugin-session-sharing 4.7.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/controllers.js +33 -35
- package/library.js +321 -378
- package/package.json +5 -3
- package/plugin.json +3 -2
- package/static/lib/admin.js +30 -28
- package/static/templates/admin/plugins/session-sharing.tpl +30 -1
- package/test/index.js +183 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-session-sharing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Allows login sessions from your app to persist in NodeBB",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@commitlint/cli": "^9.1.2",
|
|
40
40
|
"async": "^2",
|
|
41
41
|
"eslint": "^7.7.0",
|
|
42
|
-
"jsonwebtoken": "
|
|
42
|
+
"jsonwebtoken": "^8.5.1",
|
|
43
43
|
"lint-staged": "^10.0.9",
|
|
44
44
|
"lodash": "^4.17.14"
|
|
45
45
|
},
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"eslint-config-airbnb-base": "^12.1.0",
|
|
51
51
|
"eslint-plugin-import": "^2.8.0",
|
|
52
52
|
"husky": "^2.4.0",
|
|
53
|
-
"lint-staged": "^10.0.9"
|
|
53
|
+
"lint-staged": "^10.0.9",
|
|
54
|
+
"request": "^2.88.2",
|
|
55
|
+
"request-promise-native": "^1.0.9"
|
|
54
56
|
}
|
|
55
57
|
}
|
package/plugin.json
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
"url": "https://github.com/NodeBB/nodebb-plugin-session-sharing",
|
|
4
4
|
"library": "./library.js",
|
|
5
5
|
"hooks": [
|
|
6
|
-
{ "hook": "
|
|
6
|
+
{ "hook": "response:router.page", "method": "addMiddleware" },
|
|
7
7
|
{ "hook": "static:app.load", "method": "init" },
|
|
8
8
|
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
9
9
|
{ "hook": "static:user.loggedOut", "method": "cleanup" },
|
|
10
10
|
{ "hook": "filter:config.get", "method": "appendConfig" },
|
|
11
11
|
{ "hook": "filter:middleware.render", "method": "appendTemplate" },
|
|
12
|
-
{ "hook": "action:settings.set", "method": "reloadSettings" }
|
|
12
|
+
{ "hook": "action:settings.set", "method": "reloadSettings" },
|
|
13
|
+
{ "hook": "action:login.continue", "method": "saveReverseToken" }
|
|
13
14
|
],
|
|
14
15
|
"less": [
|
|
15
16
|
"static/style.less"
|
package/static/lib/admin.js
CHANGED
|
@@ -34,35 +34,37 @@ define('admin/plugins/session-sharing', ['settings'], function (Settings) {
|
|
|
34
34
|
|
|
35
35
|
ACP._searchDelay = setTimeout(function () {
|
|
36
36
|
delete ACP._searchDelay;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
|
|
38
|
+
var qs = decodeURIComponent($.param({
|
|
39
39
|
query: element.val(),
|
|
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
|
-
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
$.get(config.relative_path + '/api/admin/manage/users?' + qs)
|
|
43
|
+
.then(function (results) {
|
|
44
|
+
var resultEl = $('#result');
|
|
45
|
+
|
|
46
|
+
if (results.users.length) {
|
|
47
|
+
socket.emit('plugins.sessionSharing.showUserIds', {
|
|
48
|
+
uids: results.users.map(function (user) {
|
|
49
|
+
return user.uid;
|
|
50
|
+
}),
|
|
51
|
+
}, function (err, remoteIds) {
|
|
52
|
+
if (err) {
|
|
53
|
+
resultEl.text('We encountered an error while servicing this request:' + err.message);
|
|
54
|
+
} else {
|
|
55
|
+
resultEl.empty();
|
|
56
|
+
results.users.forEach(function (userObj, idx) {
|
|
57
|
+
resultEl.append('<p>Username: ' + userObj.username + '<br />NodeBB uid: ' + userObj.uid + '<br />Remote id: ' + (remoteIds[idx] || '<em>Not Found</em>'));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
resultEl.text('No users matched your query');
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.fail(function (err) {
|
|
66
|
+
$('#result').text('We encountered an error while servicing this request:' + err.message);
|
|
67
|
+
});
|
|
66
68
|
}, 500);
|
|
67
69
|
};
|
|
68
70
|
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
<select class="form-control" name="behaviour" id="behaviour">
|
|
47
47
|
<option value="trust">"Trust" → Shared cookie token used once only to authenticate, session persists even if cookie cleared</option>
|
|
48
48
|
<option value="revalidate">"Revalidate" → Shared cookie is checked on every page load, and updated/logged out to reflect changes in cookie</option>
|
|
49
|
+
<option value="update">"Update" → Shared cookie is checked on every page load, and updated to reflect changes in cookie. But user is not logged out when cookie is missing</option>
|
|
49
50
|
</select>
|
|
50
51
|
</div>
|
|
51
52
|
<div class="checkbox">
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
<span class="mdl-switch__label"><strong>Apply revalidation rules to administrators as well</strong></span>
|
|
55
56
|
</label>
|
|
56
57
|
<p class="help-block">
|
|
57
|
-
Administrators are exempt from the <code>revalidate</code> behaviour because a
|
|
58
|
+
Administrators are exempt from the <code>revalidate</code> behaviour because a
|
|
58
59
|
misconfiguration could lock them out of the admin panel. Enable this option to force
|
|
59
60
|
administrators to also undergo cookie revalidation, and thereby increasing security.
|
|
60
61
|
</p>
|
|
@@ -82,6 +83,15 @@
|
|
|
82
83
|
NodeBB to automatically sync up the local profile with the information provided.
|
|
83
84
|
</div>
|
|
84
85
|
</div>
|
|
86
|
+
<div class="checkbox">
|
|
87
|
+
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
|
88
|
+
<input class="mdl-switch__input" type="checkbox" id="allowBannedUsers" name="allowBannedUsers" />
|
|
89
|
+
<span class="mdl-switch__label"><strong>Allow banned users to be logged in and placed into banned user group</strong></span>
|
|
90
|
+
</label>
|
|
91
|
+
<p class="help-block">
|
|
92
|
+
By default banned users arent logged in and an error is thrown. If enabled, banned users are logged in and placed in the banned group.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
85
95
|
<div class="checkbox">
|
|
86
96
|
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
|
87
97
|
<input class="mdl-switch__input" type="checkbox" id="syncGroupJoin" name="syncGroupJoin" />
|
|
@@ -232,6 +242,25 @@
|
|
|
232
242
|
</div>
|
|
233
243
|
</div>
|
|
234
244
|
</div>
|
|
245
|
+
<div class="row">
|
|
246
|
+
<div class="col-sm-2 col-xs-12 settings-header">Reverse Token</div>
|
|
247
|
+
<div class="col-sm-10 col-xs-12">
|
|
248
|
+
<div class="form-group">
|
|
249
|
+
<div class="checkbox">
|
|
250
|
+
<label for="reverseToken" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
|
251
|
+
<input type="checkbox" class="mdl-switch__input" id="reverseToken" name="reverseToken">
|
|
252
|
+
<span class="mdl-switch__label"><strong>Enabled</strong></span>
|
|
253
|
+
</label>
|
|
254
|
+
</div>
|
|
255
|
+
<p class="help-block">
|
|
256
|
+
If enabled, NodeBB will save a cookie called <code>nbb_token</code> that can be read by other sites on the same domain. It will allow other sites to authenticate a user session based on NodeBB login state.
|
|
257
|
+
</p>
|
|
258
|
+
<p class="help-block">
|
|
259
|
+
Similar to what is accepted by the session-sharing plugin, the reverse token is a signed <a href="//jwt.io">JWT</a> containing the logged in user's username and uid. Guests will not have an `nbb_token`.
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
235
264
|
</div>
|
|
236
265
|
<div class="col-md-3">
|
|
237
266
|
<div class="panel panel-default">
|
package/test/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* globals describe, it */
|
|
4
|
+
|
|
5
|
+
// MUST BE RUN WITH ENV VAR "TEST_ENV=development"
|
|
6
|
+
// Don't forget to add this plugin to `test_plugins` array in `config.json`
|
|
7
|
+
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const url = require('url');
|
|
10
|
+
const util = require('util');
|
|
11
|
+
const request = require('request-promise-native');
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line no-unused-vars
|
|
14
|
+
const db = require.main.require('./test/mocks/databasemock');
|
|
15
|
+
|
|
16
|
+
const nconf = require.main.require('nconf');
|
|
17
|
+
const meta = require.main.require('./src/meta');
|
|
18
|
+
|
|
19
|
+
describe('nodebb-plugin-session-sharing', () => {
|
|
20
|
+
const userJar = request.jar();
|
|
21
|
+
const anonJar = request.jar();
|
|
22
|
+
const { protocol, hostname } = url.parse(nconf.get('url'));
|
|
23
|
+
|
|
24
|
+
describe('debug route', () => {
|
|
25
|
+
it('should 404 when no secret is set', async () => {
|
|
26
|
+
const response = await request(`${nconf.get('url')}/debug/session`, {
|
|
27
|
+
jar: userJar,
|
|
28
|
+
resolveWithFullResponse: true,
|
|
29
|
+
simple: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(response.statusCode, 404);
|
|
33
|
+
|
|
34
|
+
await meta.settings.setOne('session-sharing', 'secret', 's3cr37c47');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should generate a valid session when called', async () => {
|
|
38
|
+
const body = await request(`${nconf.get('url')}/debug/session`, {
|
|
39
|
+
jar: userJar,
|
|
40
|
+
});
|
|
41
|
+
assert.strictEqual(body, 'OK');
|
|
42
|
+
|
|
43
|
+
const cookies = userJar.getCookies(`${protocol}//${hostname}/`);
|
|
44
|
+
assert(cookies);
|
|
45
|
+
assert(cookies.some(cookie => cookie.key === 'token'));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('token processing middleware', () => {
|
|
50
|
+
it('should automatically log in a user with a valid token', async () => {
|
|
51
|
+
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
52
|
+
const response = await request(`${nconf.get('url')}`, {
|
|
53
|
+
resolveWithFullResponse: true,
|
|
54
|
+
jar: userJar,
|
|
55
|
+
});
|
|
56
|
+
assert.strictEqual(response.statusCode, 200);
|
|
57
|
+
|
|
58
|
+
const cookies = userJar.getCookies(`${protocol}//${hostname}/`);
|
|
59
|
+
assert(cookies);
|
|
60
|
+
|
|
61
|
+
const sidCookie = cookies.find(cookie => cookie.key === 'express.sid');
|
|
62
|
+
assert(sidCookie);
|
|
63
|
+
|
|
64
|
+
let sid = sidCookie.value.match(/s%3A([^.]+)/);
|
|
65
|
+
assert(sid && sid[1]);
|
|
66
|
+
sid = sid[1];
|
|
67
|
+
const sessionObj = await getSession(sid);
|
|
68
|
+
|
|
69
|
+
assert(sessionObj && sessionObj.passport && sessionObj.passport.user);
|
|
70
|
+
assert(parseInt(sessionObj.passport.user, 10) > 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should transparently pass-through as guest without a token', async () => {
|
|
74
|
+
const response = await request(`${nconf.get('url')}`, {
|
|
75
|
+
resolveWithFullResponse: true,
|
|
76
|
+
jar: anonJar,
|
|
77
|
+
});
|
|
78
|
+
assert.strictEqual(response.statusCode, 200);
|
|
79
|
+
|
|
80
|
+
const cookies = anonJar.getCookies(`${protocol}//${hostname}/`);
|
|
81
|
+
assert(cookies.every(cookie => cookie.key !== 'express.sid'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should redirect a guest to a specified redirection target if configured', async () => {
|
|
85
|
+
await meta.settings.setOne('session-sharing', 'guestRedirect', 'https://example.org');
|
|
86
|
+
const response = await request(`${nconf.get('url')}`, {
|
|
87
|
+
resolveWithFullResponse: true,
|
|
88
|
+
jar: anonJar,
|
|
89
|
+
followRedirect: false,
|
|
90
|
+
simple: false,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert(response.statusCode, 302);
|
|
94
|
+
assert.strictEqual(response.headers.location, 'https://example.org');
|
|
95
|
+
await meta.settings.setOne('session-sharing', 'guestRedirect', '');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should maintain the login if behaviour is "revalidate"', async () => {
|
|
99
|
+
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
100
|
+
await meta.settings.setOne('session-sharing', 'behaviour', 'revalidate');
|
|
101
|
+
|
|
102
|
+
await request(`${nconf.get('url')}`, {
|
|
103
|
+
resolveWithFullResponse: true,
|
|
104
|
+
jar: userJar, // now with no token
|
|
105
|
+
followRedirect: false,
|
|
106
|
+
simple: false,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const cookies = userJar.getCookies(`${protocol}//${hostname}/`);
|
|
110
|
+
assert(cookies);
|
|
111
|
+
|
|
112
|
+
const sidCookie = cookies.find(cookie => cookie.key === 'express.sid');
|
|
113
|
+
assert(sidCookie);
|
|
114
|
+
|
|
115
|
+
let sid = sidCookie.value.match(/s%3A([^.]+)/);
|
|
116
|
+
assert(sid && sid[1]);
|
|
117
|
+
sid = sid[1];
|
|
118
|
+
const sessionObj = await getSession(sid);
|
|
119
|
+
assert(sessionObj && sessionObj.passport && sessionObj.passport.user);
|
|
120
|
+
assert(parseInt(sessionObj.passport.user, 10) > 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should log the user out if behaviour is "revalidate" and the token is gone', async () => {
|
|
124
|
+
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
125
|
+
|
|
126
|
+
userJar.setCookie('token=', `${protocol}//${hostname}/`); // remove the token
|
|
127
|
+
await request(`${nconf.get('url')}`, {
|
|
128
|
+
resolveWithFullResponse: true,
|
|
129
|
+
jar: userJar, // now with no token
|
|
130
|
+
followRedirect: false,
|
|
131
|
+
simple: false,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const cookies = userJar.getCookies(`${protocol}//${hostname}/`);
|
|
135
|
+
assert(cookies);
|
|
136
|
+
|
|
137
|
+
const sidCookie = cookies.find(cookie => cookie.key === 'express.sid');
|
|
138
|
+
assert(sidCookie);
|
|
139
|
+
|
|
140
|
+
let sid = sidCookie.value.match(/s%3A([^.]+)/);
|
|
141
|
+
assert(sid && sid[1]);
|
|
142
|
+
sid = sid[1];
|
|
143
|
+
const sessionObj = await getSession(sid);
|
|
144
|
+
|
|
145
|
+
assert(sessionObj && (!sessionObj.passport || !sessionObj.passport.uid));
|
|
146
|
+
|
|
147
|
+
// Restore userJar's session again
|
|
148
|
+
await request(`${nconf.get('url')}/debug/session`, {
|
|
149
|
+
jar: userJar,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('login override', () => {
|
|
155
|
+
it('should redirect a guest to a specified login override if configured', async () => {
|
|
156
|
+
await meta.settings.setOne('session-sharing', 'loginOverride', 'https://example.org/login');
|
|
157
|
+
const response = await request(`${nconf.get('url')}/login`, {
|
|
158
|
+
resolveWithFullResponse: true,
|
|
159
|
+
jar: anonJar,
|
|
160
|
+
followRedirect: false,
|
|
161
|
+
simple: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert(response.statusCode, 302);
|
|
165
|
+
assert.strictEqual(response.headers.location, 'https://example.org/login');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('register override', () => {
|
|
170
|
+
it('should redirect a guest to a specified register override if configured', async () => {
|
|
171
|
+
await meta.settings.setOne('session-sharing', 'registerOverride', 'https://example.org/register');
|
|
172
|
+
const response = await request(`${nconf.get('url')}/register`, {
|
|
173
|
+
resolveWithFullResponse: true,
|
|
174
|
+
jar: anonJar,
|
|
175
|
+
followRedirect: false,
|
|
176
|
+
simple: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
assert(response.statusCode, 302);
|
|
180
|
+
assert.strictEqual(response.headers.location, 'https://example.org/register');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|