nodebb-plugin-session-sharing 4.8.0 → 5.1.2
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 +30 -28
- package/library.js +296 -343
- 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 +29 -1
- package/test/index.js +183 -0
package/lib/controllers.js
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const groups = require.main.require('./src/groups');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const Controllers = {};
|
|
6
6
|
|
|
7
7
|
Controllers.renderAdminPage = async (req, res) => {
|
|
8
8
|
const groupData = await groups.getGroupsFromSet('groups:visible:createtime', 0, -1);
|
|
9
9
|
res.render('admin/plugins/session-sharing', { groups: groupData });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
Controllers.retrieveUser =
|
|
12
|
+
Controllers.retrieveUser = async (req, res) => {
|
|
13
13
|
const main = module.parent.exports;
|
|
14
14
|
const remoteId = req.query.id;
|
|
15
|
-
|
|
16
|
-
if (remoteId) {
|
|
17
|
-
|
|
18
|
-
if (err) {
|
|
19
|
-
res.status(500).json({
|
|
20
|
-
error: err.message,
|
|
21
|
-
});
|
|
22
|
-
} else if (userObj) {
|
|
23
|
-
res.status(200).json(userObj);
|
|
24
|
-
} else {
|
|
25
|
-
res.sendStatus(404);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
} else {
|
|
29
|
-
res.status(400).json({
|
|
15
|
+
|
|
16
|
+
if (!remoteId) {
|
|
17
|
+
return res.status(400).json({
|
|
30
18
|
error: 'no-id-supplied',
|
|
31
19
|
});
|
|
32
20
|
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const userObj = await main.getUser(remoteId);
|
|
24
|
+
|
|
25
|
+
if (!userObj) {
|
|
26
|
+
return res.sendStatus(404);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return res.status(200).json(userObj);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return res.status(500).json({
|
|
32
|
+
error: error.message,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
Controllers.process =
|
|
37
|
+
Controllers.process = async (req, res) => {
|
|
36
38
|
const main = module.parent.exports;
|
|
37
39
|
|
|
38
40
|
if (!req.body || !req.body.token) {
|
|
@@ -40,18 +42,18 @@ Controllers.process = function (req, res) {
|
|
|
40
42
|
error: 'no-token-provided',
|
|
41
43
|
});
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const uid = await main.process(req.body.token);
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
if (err) {
|
|
46
|
-
return res.status(500).json({
|
|
47
|
-
error: err.message,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
res.status(200).json({
|
|
49
|
+
return res.status(200).json({
|
|
52
50
|
uid,
|
|
53
51
|
});
|
|
54
|
-
})
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return res.status(500).json({
|
|
54
|
+
error: error.message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
module.exports = Controllers;
|
package/library.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var groups = require.main.require('./src/groups');
|
|
6
|
-
var SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
3
|
+
const winston = module.parent.require('winston');
|
|
4
|
+
const nconf = module.parent.require('nconf');
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
var nconf = module.parent.require('nconf');
|
|
6
|
+
const _ = require('lodash');
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const meta = require.main.require('./src/meta');
|
|
9
|
+
const user = require.main.require('./src/user');
|
|
10
|
+
const groups = require.main.require('./src/groups');
|
|
11
|
+
const SocketPlugins = require.main.require('./src/socket.io/plugins');
|
|
12
|
+
const db = require.main.require('./src/database');
|
|
13
|
+
const plugins = require.main.require('./src/plugins');
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
var plugins = require.main.require('./src/plugins');
|
|
15
|
+
const jwt = require('jsonwebtoken');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var controllers = require('./lib/controllers');
|
|
20
|
-
var nbbAuthController = require.main.require('./src/controllers/authentication');
|
|
17
|
+
const controllers = require('./lib/controllers');
|
|
18
|
+
const nbbAuthController = require.main.require('./src/controllers/authentication');
|
|
21
19
|
|
|
22
20
|
/* all the user profile fields that can be passed to user.updateProfile */
|
|
23
|
-
|
|
21
|
+
const profileFields = [
|
|
24
22
|
'username',
|
|
25
23
|
'email',
|
|
26
24
|
'fullname',
|
|
@@ -31,7 +29,7 @@ var profileFields = [
|
|
|
31
29
|
'signature',
|
|
32
30
|
'aboutme',
|
|
33
31
|
];
|
|
34
|
-
|
|
32
|
+
const payloadKeys = profileFields.concat([
|
|
35
33
|
'id', // the uniq identifier of that account
|
|
36
34
|
'firstName', // for backwards compatibillity
|
|
37
35
|
'lastName', // dto.
|
|
@@ -39,7 +37,7 @@ var payloadKeys = profileFields.concat([
|
|
|
39
37
|
'groups',
|
|
40
38
|
]);
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
const plugin = {
|
|
43
41
|
ready: false,
|
|
44
42
|
settings: {
|
|
45
43
|
name: 'appId',
|
|
@@ -50,6 +48,7 @@ var plugin = {
|
|
|
50
48
|
adminRevalidate: 'off',
|
|
51
49
|
noRegistration: 'off',
|
|
52
50
|
payloadParent: undefined,
|
|
51
|
+
allowBannedUsers: false,
|
|
53
52
|
},
|
|
54
53
|
};
|
|
55
54
|
|
|
@@ -74,7 +73,7 @@ plugin.init = async (params) => {
|
|
|
74
73
|
await plugin.reloadSettings();
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
plugin.appendConfig =
|
|
76
|
+
plugin.appendConfig = async (config) => {
|
|
78
77
|
config.sessionSharing = {
|
|
79
78
|
logoutRedirect: plugin.settings.logoutRedirect,
|
|
80
79
|
loginOverride: plugin.settings.loginOverride,
|
|
@@ -83,45 +82,30 @@ plugin.appendConfig = function (config, callback) {
|
|
|
83
82
|
hostWhitelist: plugin.settings.hostWhitelist,
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
return config;
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
/* Websocket Listeners */
|
|
90
89
|
|
|
91
90
|
SocketPlugins.sessionSharing = {};
|
|
92
91
|
|
|
93
|
-
SocketPlugins.sessionSharing.showUserIds =
|
|
92
|
+
SocketPlugins.sessionSharing.showUserIds = async (socket, data) => {
|
|
94
93
|
// Retrieve the hash and find matches
|
|
95
|
-
|
|
96
|
-
var payload = [];
|
|
97
|
-
|
|
98
|
-
payload.length = uids.length;
|
|
99
|
-
|
|
100
|
-
if (uids.length) {
|
|
101
|
-
async.map(uids, function (uid, next) {
|
|
102
|
-
db.getSortedSetRangeByScore(plugin.settings.name + ':uid', 0, -1, uid, uid, next);
|
|
103
|
-
}, function (err, remoteIds) {
|
|
104
|
-
if (err) {
|
|
105
|
-
return callback(err);
|
|
106
|
-
}
|
|
94
|
+
const { uids } = data;
|
|
107
95
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
callback(null, payload);
|
|
113
|
-
});
|
|
114
|
-
} else {
|
|
115
|
-
callback(new Error('no-uids-supplied'));
|
|
96
|
+
if (!uids.length) {
|
|
97
|
+
throw new Error('no-uids-supplied');
|
|
116
98
|
}
|
|
99
|
+
|
|
100
|
+
return Promise.all(uids.map(async uid => db.getSortedSetRangeByScore(plugin.settings.name + ':uid', 0, -1, uid, uid)));
|
|
117
101
|
};
|
|
118
102
|
|
|
119
|
-
SocketPlugins.sessionSharing.findUserByRemoteId =
|
|
120
|
-
if (data.remoteId) {
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
callback(new Error('no-remote-id-supplied'));
|
|
103
|
+
SocketPlugins.sessionSharing.findUserByRemoteId = async (socket, data) => {
|
|
104
|
+
if (!data.remoteId) {
|
|
105
|
+
throw new Error('no-remote-id-supplied');
|
|
124
106
|
}
|
|
107
|
+
|
|
108
|
+
return plugin.getUser(data.remoteId);
|
|
125
109
|
};
|
|
126
110
|
|
|
127
111
|
/* End Websocket Listeners */
|
|
@@ -129,32 +113,28 @@ SocketPlugins.sessionSharing.findUserByRemoteId = function (socket, data, callba
|
|
|
129
113
|
/*
|
|
130
114
|
* Given a remoteId, show user data
|
|
131
115
|
*/
|
|
132
|
-
plugin.getUser =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
], callback);
|
|
116
|
+
plugin.getUser = async (remoteId) => {
|
|
117
|
+
const uid = await db.sortedSetScore(plugin.settings.name + ':uid', remoteId);
|
|
118
|
+
|
|
119
|
+
if (!uid) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return user.getUserFields(uid, ['username', 'userslug', 'picture']);
|
|
143
124
|
};
|
|
144
125
|
|
|
145
|
-
plugin.process =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
], callback);
|
|
126
|
+
plugin.process = async (token) => {
|
|
127
|
+
const payload = await jwt.verify(token, plugin.settings.secret);
|
|
128
|
+
const userData = await plugin.normalizePayload(payload);
|
|
129
|
+
const [uid, isNewUser] = await plugin.findOrCreateUser(userData);
|
|
130
|
+
await plugin.updateUserProfile(uid, userData, isNewUser);
|
|
131
|
+
await plugin.updateUserGroups(uid, userData);
|
|
132
|
+
await plugin.verifyUser(token, uid, isNewUser);
|
|
133
|
+
return uid;
|
|
154
134
|
};
|
|
155
135
|
|
|
156
|
-
plugin.normalizePayload =
|
|
157
|
-
|
|
136
|
+
plugin.normalizePayload = async (payload) => {
|
|
137
|
+
const userData = {};
|
|
158
138
|
|
|
159
139
|
if (plugin.settings.payloadParent) {
|
|
160
140
|
payload = payload[plugin.settings.payloadParent];
|
|
@@ -162,11 +142,11 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
162
142
|
|
|
163
143
|
if (typeof payload !== 'object') {
|
|
164
144
|
winston.warn('[session-sharing] the payload is not an object', payload);
|
|
165
|
-
|
|
145
|
+
throw new Error('payload-invalid');
|
|
166
146
|
}
|
|
167
147
|
|
|
168
148
|
payloadKeys.forEach(function (key) {
|
|
169
|
-
|
|
149
|
+
const propName = plugin.settings['payload:' + key];
|
|
170
150
|
if (payload[propName]) {
|
|
171
151
|
userData[key] = payload[propName];
|
|
172
152
|
}
|
|
@@ -174,7 +154,7 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
174
154
|
|
|
175
155
|
if (!userData.id) {
|
|
176
156
|
winston.warn('[session-sharing] No user id was given in payload');
|
|
177
|
-
|
|
157
|
+
throw new Error('payload-invalid');
|
|
178
158
|
}
|
|
179
159
|
|
|
180
160
|
userData.fullname = (userData.fullname || [userData.firstName, userData.lastName].join(' ')).trim();
|
|
@@ -188,7 +168,7 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
188
168
|
|
|
189
169
|
if (!userData.username) {
|
|
190
170
|
winston.warn('[session-sharing] No valid username could be determined');
|
|
191
|
-
|
|
171
|
+
throw new Error('payload-invalid');
|
|
192
172
|
}
|
|
193
173
|
|
|
194
174
|
if (userData.hasOwnProperty('groups') && !Array.isArray(userData.groups)) {
|
|
@@ -197,218 +177,185 @@ plugin.normalizePayload = function (payload, callback) {
|
|
|
197
177
|
}
|
|
198
178
|
|
|
199
179
|
winston.verbose('[session-sharing] Payload verified');
|
|
200
|
-
plugins.hooks.fire('filter:sessionSharing.normalizePayload', {
|
|
180
|
+
const data = await plugins.hooks.fire('filter:sessionSharing.normalizePayload', {
|
|
201
181
|
payload: payload,
|
|
202
182
|
userData: userData,
|
|
203
|
-
}, function (err, data) {
|
|
204
|
-
callback(err, data.userData);
|
|
205
183
|
});
|
|
184
|
+
|
|
185
|
+
return data.userData;
|
|
206
186
|
};
|
|
207
187
|
|
|
208
|
-
plugin.verifyUser =
|
|
209
|
-
plugins.hooks.fire('static:sessionSharing.verifyUser', {
|
|
188
|
+
plugin.verifyUser = async (token, uid, isNewUser) => {
|
|
189
|
+
await plugins.hooks.fire('static:sessionSharing.verifyUser', {
|
|
210
190
|
uid: uid,
|
|
211
191
|
isNewUser: isNewUser,
|
|
212
192
|
token: token,
|
|
213
|
-
}, function (err) {
|
|
214
|
-
if (err) {
|
|
215
|
-
return callback(err);
|
|
216
|
-
}
|
|
217
|
-
// Check ban state of user, reject if banned
|
|
218
|
-
user.bans.isBanned(uid, function (err, banned) {
|
|
219
|
-
callback(err || banned ? new Error('banned') : null, uid);
|
|
220
|
-
});
|
|
221
193
|
});
|
|
194
|
+
|
|
195
|
+
// Check ban state of user
|
|
196
|
+
const isBanned = await user.bans.isBanned(uid);
|
|
197
|
+
|
|
198
|
+
// Reject if banned and settings dont allow banned users to login
|
|
199
|
+
if (isBanned && !plugin.settings.allowBannedUsers) {
|
|
200
|
+
throw new Error('banned');
|
|
201
|
+
}
|
|
222
202
|
};
|
|
223
203
|
|
|
224
|
-
plugin.findOrCreateUser =
|
|
225
|
-
|
|
204
|
+
plugin.findOrCreateUser = async (userData) => {
|
|
205
|
+
const { id } = userData;
|
|
206
|
+
let isNewUser = false;
|
|
207
|
+
let userId = null;
|
|
208
|
+
let queries = [db.sortedSetScore(plugin.settings.name + ':uid', userData.id)];
|
|
209
|
+
|
|
226
210
|
if (userData.email && userData.email.length) {
|
|
227
|
-
queries
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return plugin.createUser(userData, function (err, uid) {
|
|
271
|
-
next(err, uid, userData, true);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
setImmediate(next, null, uid, userData, false);
|
|
275
|
-
},
|
|
276
|
-
], callback);
|
|
277
|
-
});
|
|
211
|
+
queries = [...queries, db.sortedSetScore('email:uid', userData.email)];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let [uid, mergeUid] = await Promise.all(queries);
|
|
215
|
+
uid = parseInt(uid, 10);
|
|
216
|
+
mergeUid = parseInt(mergeUid, 10);
|
|
217
|
+
|
|
218
|
+
/* check if found something to work with */
|
|
219
|
+
if (uid && !isNaN(uid)) {
|
|
220
|
+
try {
|
|
221
|
+
/* check if the user with the given id actually exists */
|
|
222
|
+
const exists = await user.exists(uid);
|
|
223
|
+
|
|
224
|
+
if (exists) {
|
|
225
|
+
userId = uid;
|
|
226
|
+
} else {
|
|
227
|
+
/* reference is outdated, user got deleted */
|
|
228
|
+
await db.sortedSetRemove(plugin.settings.name + ':uid', id);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
/* ignore errors, but assume the user doesn't exist */
|
|
232
|
+
winston.warn('[session-sharing] Error while testing user existance', error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!userId && mergeUid && !isNaN(mergeUid)) {
|
|
237
|
+
winston.info('[session-sharing] Found user via their email, associating this id (' + id + ') with their NodeBB account');
|
|
238
|
+
await db.sortedSetAdd(plugin.settings.name + ':uid', mergeUid, id);
|
|
239
|
+
userId = mergeUid;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* create the user from payload if necessary */
|
|
243
|
+
winston.debug('createUser?', !userId);
|
|
244
|
+
if (!userId) {
|
|
245
|
+
if (plugin.settings.noRegistration === 'on') {
|
|
246
|
+
throw new Error('no-match');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
userId = await plugin.createUser(userData);
|
|
250
|
+
isNewUser = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [userId, isNewUser];
|
|
278
254
|
};
|
|
279
255
|
|
|
280
|
-
plugin.updateUserProfile =
|
|
256
|
+
plugin.updateUserProfile = async (uid, userData, isNewUser) => {
|
|
281
257
|
winston.debug('consider updateProfile?', isNewUser || plugin.settings.updateProfile === 'on');
|
|
258
|
+
let userObj = {};
|
|
282
259
|
|
|
283
260
|
/* even update the profile on a new account, since some fields are not initialized by NodeBB */
|
|
284
261
|
if (!isNewUser && plugin.settings.updateProfile !== 'on') {
|
|
285
|
-
return
|
|
262
|
+
return;
|
|
286
263
|
}
|
|
287
264
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
var obj = {};
|
|
265
|
+
const existingFields = await user.getUserFields(uid, profileFields);
|
|
266
|
+
const obj = profileFields.reduce((result, field) => {
|
|
267
|
+
if (typeof userData[field] !== 'undefined' && existingFields[field] !== userData[field]) {
|
|
268
|
+
result[field] = userData[field];
|
|
269
|
+
}
|
|
294
270
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
obj[field] = userData[field];
|
|
298
|
-
}
|
|
299
|
-
});
|
|
271
|
+
return result;
|
|
272
|
+
}, {});
|
|
300
273
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
next(null, userObj || existingFields);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
setImmediate(next, null, {});
|
|
314
|
-
},
|
|
315
|
-
function (userObj, next) {
|
|
316
|
-
if (userData.picture) {
|
|
317
|
-
return db.setObjectField('user:' + uid, 'picture', userData.picture, next);
|
|
274
|
+
if (Object.keys(obj).length) {
|
|
275
|
+
winston.debug('[session-sharing] Updating profile fields:', obj);
|
|
276
|
+
obj.uid = uid;
|
|
277
|
+
try {
|
|
278
|
+
userObj = await user.updateProfile(uid, obj);
|
|
279
|
+
|
|
280
|
+
// If it errors out, not that big of a deal, continue anyway.
|
|
281
|
+
if (!userObj) {
|
|
282
|
+
userObj = existingFields;
|
|
318
283
|
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
winston.warn('[session-sharing] Unable to update profile information for uid: ' + uid + '(' + error.message + ')');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
319
288
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return callback(err, uid, userData, isNewUser);
|
|
324
|
-
});
|
|
289
|
+
if (userData.picture) {
|
|
290
|
+
await db.setObjectField('user:' + uid, 'picture', userData.picture);
|
|
291
|
+
}
|
|
325
292
|
};
|
|
326
293
|
|
|
327
|
-
plugin.updateUserGroups =
|
|
294
|
+
plugin.updateUserGroups = async (uid, userData) => {
|
|
328
295
|
if (!userData.groups || !Array.isArray(userData.groups)) {
|
|
329
|
-
return
|
|
296
|
+
return;
|
|
330
297
|
}
|
|
331
298
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// Normalize user group data to just group names
|
|
337
|
-
groups = groups[0].map(function (groupObj) {
|
|
338
|
-
return groupObj.name;
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// Build join and leave arrays
|
|
342
|
-
var join = userData.groups.filter(function (name) {
|
|
343
|
-
return !groups.includes(name);
|
|
344
|
-
});
|
|
299
|
+
// Retrieve user groups
|
|
300
|
+
let [userGroups] = await groups.getUserGroupsFromSet('groups:createtime', [uid]);
|
|
301
|
+
// Normalize user group data to just group names
|
|
302
|
+
userGroups = userGroups.map(groupObj => groupObj.name);
|
|
345
303
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// `registered-users` is always a joined group
|
|
352
|
-
if (name === 'registered-users') {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return !userData.groups.includes(name);
|
|
357
|
-
});
|
|
304
|
+
// Build join and leave arrays
|
|
305
|
+
let join = userData.groups.filter(name => !userGroups.includes(name));
|
|
306
|
+
if (plugin.settings.syncGroupList === 'on') {
|
|
307
|
+
join = join.filter(group => plugin.settings.syncGroups.includes(group));
|
|
308
|
+
}
|
|
358
309
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
310
|
+
let leave = userGroups.filter((name) => {
|
|
311
|
+
// `registered-users` is always a joined group
|
|
312
|
+
if (name === 'registered-users') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
362
315
|
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
], function (err) {
|
|
366
|
-
return callback(err, uid, isNewUser);
|
|
316
|
+
return !userData.groups.includes(name);
|
|
367
317
|
});
|
|
318
|
+
if (plugin.settings.syncGroupList === 'on') {
|
|
319
|
+
leave = leave.filter(group => plugin.settings.syncGroups.includes(group));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await executeJoinLeave(uid, join, leave);
|
|
368
323
|
};
|
|
369
324
|
|
|
370
|
-
function executeJoinLeave(uid, join, leave
|
|
371
|
-
|
|
372
|
-
|
|
325
|
+
async function executeJoinLeave(uid, join, leave) {
|
|
326
|
+
await Promise.all([
|
|
327
|
+
(async () => {
|
|
373
328
|
if (plugin.settings.syncGroupJoin !== 'on') {
|
|
374
|
-
return
|
|
329
|
+
return;
|
|
375
330
|
}
|
|
376
331
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
},
|
|
381
|
-
function (next) {
|
|
332
|
+
await Promise.all(join.map(name => groups.join(name, uid)));
|
|
333
|
+
})(),
|
|
334
|
+
(async () => {
|
|
382
335
|
if (plugin.settings.syncGroupLeave !== 'on') {
|
|
383
|
-
return
|
|
336
|
+
return;
|
|
384
337
|
}
|
|
385
338
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
},
|
|
390
|
-
], callback);
|
|
339
|
+
await Promise.all(leave.map(name => groups.leave(name, uid)));
|
|
340
|
+
})(),
|
|
341
|
+
]);
|
|
391
342
|
}
|
|
392
343
|
|
|
393
|
-
plugin.createUser =
|
|
344
|
+
plugin.createUser = async (userData) => {
|
|
394
345
|
winston.verbose('[session-sharing] No user found, creating a new user for this login');
|
|
395
346
|
|
|
396
|
-
user.create(_.pick(userData, profileFields)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
db.sortedSetAdd(plugin.settings.name + ':uid', uid, userData.id, function (err) {
|
|
400
|
-
callback(err, uid);
|
|
401
|
-
});
|
|
402
|
-
});
|
|
347
|
+
const uid = await user.create(_.pick(userData, profileFields));
|
|
348
|
+
await db.sortedSetAdd(plugin.settings.name + ':uid', uid, userData.id);
|
|
349
|
+
return uid;
|
|
403
350
|
};
|
|
404
351
|
|
|
405
|
-
plugin.addMiddleware = async function (req, res
|
|
352
|
+
plugin.addMiddleware = async function ({ req, res }) {
|
|
406
353
|
const { hostWhitelist, guestRedirect, editOverride, loginOverride, registerOverride } = await meta.settings.get('session-sharing');
|
|
407
354
|
|
|
408
355
|
if (hostWhitelist) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
for (
|
|
356
|
+
const hosts = hostWhitelist.split(',') || [hostWhitelist];
|
|
357
|
+
let whitelisted = false;
|
|
358
|
+
for (const host of hosts) {
|
|
412
359
|
if (req.headers.host.includes(host)) {
|
|
413
360
|
whitelisted = true;
|
|
414
361
|
break;
|
|
@@ -416,24 +363,22 @@ plugin.addMiddleware = async function (req, res, next) {
|
|
|
416
363
|
}
|
|
417
364
|
|
|
418
365
|
if (!whitelisted) {
|
|
419
|
-
return
|
|
366
|
+
return;
|
|
420
367
|
}
|
|
421
368
|
}
|
|
422
369
|
|
|
423
|
-
function handleGuest(req, res
|
|
370
|
+
function handleGuest(req, res) {
|
|
424
371
|
if (guestRedirect && !req.originalUrl.startsWith(nconf.get('relative_path') + '/login?local=1')) {
|
|
425
372
|
// If a guest redirect is specified, follow it
|
|
426
373
|
res.redirect(guestRedirect.replace('%1', encodeURIComponent(req.protocol + '://' + req.get('host') + req.originalUrl)));
|
|
427
374
|
} else if (res.locals.fullRefresh === true) {
|
|
428
375
|
res.redirect(nconf.get('relative_path') + req.url);
|
|
429
|
-
} else {
|
|
430
|
-
next();
|
|
431
376
|
}
|
|
432
377
|
}
|
|
433
378
|
|
|
434
379
|
// Only respond to page loads by guests, not api or asset calls
|
|
435
|
-
|
|
436
|
-
|
|
380
|
+
const hasSession = req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && parseInt(req.user.uid, 10) > 0;
|
|
381
|
+
const hasLoginLock = req.session.hasOwnProperty('loginLock');
|
|
437
382
|
|
|
438
383
|
if (
|
|
439
384
|
!plugin.ready || // plugin not ready
|
|
@@ -444,7 +389,7 @@ plugin.addMiddleware = async function (req, res, next) {
|
|
|
444
389
|
// Let requests through under "update" or "revalidate" behaviour only if they're logging in for the first time
|
|
445
390
|
delete req.session.loginLock; // remove login lock for "update" or "revalidate" logins
|
|
446
391
|
|
|
447
|
-
return
|
|
392
|
+
return;
|
|
448
393
|
}
|
|
449
394
|
|
|
450
395
|
if (editOverride && hasSession && req.originalUrl.match(/\/user\/.*\/edit$/)) {
|
|
@@ -458,89 +403,78 @@ plugin.addMiddleware = async function (req, res, next) {
|
|
|
458
403
|
}
|
|
459
404
|
|
|
460
405
|
// Hook into ip blacklist functionality in core
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
406
|
+
try {
|
|
407
|
+
await meta.blacklist.test(req.ip);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (hasSession) {
|
|
410
|
+
req.logout();
|
|
411
|
+
res.locals.fullRefresh = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await plugin.cleanup({ res: res });
|
|
415
|
+
return handleGuest.call(null, req, res);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (Object.keys(req.cookies).length && req.cookies.hasOwnProperty(plugin.settings.cookieName) && req.cookies[plugin.settings.cookieName].length) {
|
|
419
|
+
try {
|
|
420
|
+
const uid = await plugin.process(req.cookies[plugin.settings.cookieName]);
|
|
421
|
+
winston.verbose('[session-sharing] Processing login for uid ' + uid + ', path ' + req.originalUrl);
|
|
422
|
+
req.uid = uid;
|
|
423
|
+
|
|
424
|
+
if (plugin.settings.behaviour === 'revalidate') {
|
|
425
|
+
res.locals.reroll = false; // disable session rerolling in core
|
|
466
426
|
}
|
|
467
427
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return plugins.hooks.fire('filter:sessionSharing.error', {
|
|
497
|
-
error: err,
|
|
498
|
-
uid: uid,
|
|
499
|
-
res: res,
|
|
500
|
-
settings: plugin.settings,
|
|
501
|
-
handleAsGuest: handleAsGuest,
|
|
502
|
-
}, function (err, data) {
|
|
503
|
-
if (data.handleAsGuest) {
|
|
504
|
-
return handleGuest.call(err, req, res, next);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
next(err);
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
winston.verbose('[session-sharing] Processing login for uid ' + uid + ', path ' + req.originalUrl);
|
|
512
|
-
req.uid = uid;
|
|
513
|
-
|
|
514
|
-
if (plugin.settings.behaviour === 'revalidate') {
|
|
515
|
-
res.locals.reroll = false; // disable session rerolling in core
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
nbbAuthController.doLogin(req, uid, function () {
|
|
519
|
-
req.session.loginLock = true;
|
|
520
|
-
const url = req.session.returnTo || req.originalUrl.replace(nconf.get('relative_path'), '');
|
|
521
|
-
delete req.session.returnTo;
|
|
522
|
-
res.redirect(nconf.get('relative_path') + url);
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
} else if (hasSession) {
|
|
526
|
-
// Has login session but no cookie, can assume "revalidate" behaviour
|
|
527
|
-
user.isAdministrator(req.user.uid, function (err, isAdmin) {
|
|
528
|
-
if (plugin.settings.behaviour !== 'update' && (plugin.settings.adminRevalidate === 'on' || !isAdmin)) {
|
|
529
|
-
req.logout();
|
|
530
|
-
res.locals.fullRefresh = true;
|
|
531
|
-
handleGuest(req, res, next);
|
|
532
|
-
} else {
|
|
533
|
-
// Admins can bypass
|
|
534
|
-
return next(err);
|
|
535
|
-
}
|
|
428
|
+
await nbbAuthController.doLogin(req, uid);
|
|
429
|
+
|
|
430
|
+
req.session.loginLock = true;
|
|
431
|
+
const url = req.session.returnTo || req.originalUrl.replace(nconf.get('relative_path'), '');
|
|
432
|
+
delete req.session.returnTo;
|
|
433
|
+
res.redirect(nconf.get('relative_path') + url);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
let handleAsGuest = false;
|
|
436
|
+
|
|
437
|
+
switch (error.message) {
|
|
438
|
+
case 'payload-invalid':
|
|
439
|
+
winston.warn('[session-sharing] The passed-in payload was invalid and could not be processed');
|
|
440
|
+
break;
|
|
441
|
+
case 'no-match':
|
|
442
|
+
winston.info('[session-sharing] Payload valid, but local account not found. Assuming guest.');
|
|
443
|
+
handleAsGuest = true;
|
|
444
|
+
break;
|
|
445
|
+
default:
|
|
446
|
+
winston.warn('[session-sharing] Error encountered while parsing token: ' + error.message);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const data = await plugins.hooks.fire('filter:sessionSharing.error', {
|
|
451
|
+
error,
|
|
452
|
+
res: res,
|
|
453
|
+
settings: plugin.settings,
|
|
454
|
+
handleAsGuest: handleAsGuest,
|
|
536
455
|
});
|
|
537
|
-
|
|
538
|
-
|
|
456
|
+
|
|
457
|
+
if (data.handleAsGuest) {
|
|
458
|
+
return handleGuest.call(error, req, res);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
throw error;
|
|
539
462
|
}
|
|
540
|
-
})
|
|
463
|
+
} else if (hasSession) {
|
|
464
|
+
// Has login session but no cookie, can assume "revalidate" behaviour
|
|
465
|
+
const isAdmin = await user.isAdministrator(req.user.uid);
|
|
466
|
+
|
|
467
|
+
if (plugin.settings.behaviour !== 'update' && (plugin.settings.adminRevalidate === 'on' || !isAdmin)) {
|
|
468
|
+
req.logout();
|
|
469
|
+
res.locals.fullRefresh = true;
|
|
470
|
+
return handleGuest(req, res);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
return handleGuest.call(null, req, res);
|
|
474
|
+
}
|
|
541
475
|
};
|
|
542
476
|
|
|
543
|
-
plugin.cleanup =
|
|
477
|
+
plugin.cleanup = async (data) => {
|
|
544
478
|
if (plugin.settings.cookieDomain) {
|
|
545
479
|
winston.verbose('[session-sharing] Clearing cookie');
|
|
546
480
|
data.res.clearCookie(plugin.settings.cookieName, {
|
|
@@ -550,15 +484,21 @@ plugin.cleanup = function (data, callback) {
|
|
|
550
484
|
});
|
|
551
485
|
}
|
|
552
486
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
487
|
+
data.res.clearCookie('nbb_token', {
|
|
488
|
+
domain: plugin.settings.cookieDomain,
|
|
489
|
+
expires: new Date(),
|
|
490
|
+
path: '/',
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return true;
|
|
558
494
|
};
|
|
559
495
|
|
|
560
496
|
plugin.generate = function (req, res) {
|
|
561
|
-
|
|
497
|
+
if (!plugin.ready) {
|
|
498
|
+
return res.sendStatus(404);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let payload = {};
|
|
562
502
|
payload[plugin.settings['payload:id']] = 1;
|
|
563
503
|
payload[plugin.settings['payload:username']] = 'testUser';
|
|
564
504
|
payload[plugin.settings['payload:email']] = 'testUser@example.org';
|
|
@@ -573,13 +513,13 @@ plugin.generate = function (req, res) {
|
|
|
573
513
|
payload[plugin.settings['payload:groups']] = ['test-group'];
|
|
574
514
|
|
|
575
515
|
if (plugin.settings.payloadParent || plugin.settings['payload:parent']) {
|
|
576
|
-
|
|
577
|
-
|
|
516
|
+
const parentKey = plugin.settings.payloadParent || plugin.settings['payload:parent'];
|
|
517
|
+
const newPayload = {};
|
|
578
518
|
newPayload[parentKey] = payload;
|
|
579
519
|
payload = newPayload;
|
|
580
520
|
}
|
|
581
521
|
|
|
582
|
-
|
|
522
|
+
const token = jwt.sign(payload, plugin.settings.secret);
|
|
583
523
|
res.cookie(plugin.settings.cookieName, token, {
|
|
584
524
|
maxAge: 1000 * 60 * 60 * 24 * 21,
|
|
585
525
|
httpOnly: true,
|
|
@@ -589,14 +529,14 @@ plugin.generate = function (req, res) {
|
|
|
589
529
|
res.sendStatus(200);
|
|
590
530
|
};
|
|
591
531
|
|
|
592
|
-
plugin.addAdminNavigation =
|
|
532
|
+
plugin.addAdminNavigation = async (header) => {
|
|
593
533
|
header.plugins.push({
|
|
594
534
|
route: '/plugins/session-sharing',
|
|
595
535
|
icon: 'fa-user-secret',
|
|
596
536
|
name: 'Session Sharing',
|
|
597
537
|
});
|
|
598
538
|
|
|
599
|
-
|
|
539
|
+
return header;
|
|
600
540
|
};
|
|
601
541
|
|
|
602
542
|
plugin.reloadSettings = async (data) => {
|
|
@@ -628,26 +568,39 @@ plugin.reloadSettings = async (data) => {
|
|
|
628
568
|
plugin.ready = true;
|
|
629
569
|
};
|
|
630
570
|
|
|
631
|
-
plugin.appendTemplate = (data
|
|
632
|
-
if (data.req.session.sessionSharing
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
return callback(err);
|
|
636
|
-
}
|
|
571
|
+
plugin.appendTemplate = async (data) => {
|
|
572
|
+
if (!data.req.session.sessionSharing || !data.req.session.sessionSharing.banned) {
|
|
573
|
+
return data;
|
|
574
|
+
}
|
|
637
575
|
|
|
638
|
-
|
|
639
|
-
ban: info,
|
|
640
|
-
banned: true,
|
|
641
|
-
};
|
|
576
|
+
const info = await user.getLatestBanInfo(data.req.session.sessionSharing.uid);
|
|
642
577
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
578
|
+
data.templateData.sessionSharingBan = {
|
|
579
|
+
ban: info,
|
|
580
|
+
banned: true,
|
|
581
|
+
};
|
|
646
582
|
|
|
647
|
-
|
|
583
|
+
delete data.req.session.sessionSharing;
|
|
584
|
+
return data;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
plugin.saveReverseToken = async ({ req, userData: data }) => {
|
|
588
|
+
if (!plugin.ready || plugin.settings.reverseToken !== 'on') {
|
|
589
|
+
return; // no reverse token if secret not set
|
|
648
590
|
}
|
|
649
591
|
|
|
650
|
-
|
|
592
|
+
const res = req.res;
|
|
593
|
+
const userData = await user.getUserFields(data.uid, ['uid', 'username', 'picture', 'reputation', 'postcount', 'banned']);
|
|
594
|
+
userData.groups = (await groups.getUserGroups([data.uid])).pop();
|
|
595
|
+
const token = jwt.sign(userData, plugin.settings.secret);
|
|
596
|
+
|
|
597
|
+
res.cookie('nbb_token', token, {
|
|
598
|
+
maxAge: meta.getSessionTTLSeconds() * 1000,
|
|
599
|
+
httpOnly: !req.secure,
|
|
600
|
+
domain: plugin.settings.cookieDomain,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
winston.info(`[plugins/session-sharing] Saving reverse cookie for uid ${userData.uid}, session: ${req.session.id}`);
|
|
651
604
|
};
|
|
652
605
|
|
|
653
606
|
module.exports = plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-session-sharing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.2",
|
|
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
|
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
<span class="mdl-switch__label"><strong>Apply revalidation rules to administrators as well</strong></span>
|
|
56
56
|
</label>
|
|
57
57
|
<p class="help-block">
|
|
58
|
-
Administrators are exempt from the <code>revalidate</code> behaviour because a
|
|
58
|
+
Administrators are exempt from the <code>revalidate</code> behaviour because a
|
|
59
59
|
misconfiguration could lock them out of the admin panel. Enable this option to force
|
|
60
60
|
administrators to also undergo cookie revalidation, and thereby increasing security.
|
|
61
61
|
</p>
|
|
@@ -83,6 +83,15 @@
|
|
|
83
83
|
NodeBB to automatically sync up the local profile with the information provided.
|
|
84
84
|
</div>
|
|
85
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>
|
|
86
95
|
<div class="checkbox">
|
|
87
96
|
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
|
88
97
|
<input class="mdl-switch__input" type="checkbox" id="syncGroupJoin" name="syncGroupJoin" />
|
|
@@ -233,6 +242,25 @@
|
|
|
233
242
|
</div>
|
|
234
243
|
</div>
|
|
235
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>
|
|
236
264
|
</div>
|
|
237
265
|
<div class="col-md-3">
|
|
238
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
|
+
});
|