nodebb-plugin-session-sharing 8.0.5 → 9.0.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/README.md +65 -10
- package/eslint.config.mjs +8 -0
- package/library.js +121 -14
- package/package-lock.json +15 -100
- package/package.json +2 -2
- package/static/lib/admin.js +17 -1
- package/static/templates/admin/plugins/session-sharing.tpl +49 -4
- package/test/index.js +117 -7
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Session Sharing for NodeBB
|
|
2
2
|
|
|
3
3
|
In a nutshell, this plugin allows you to share sessions between your application and NodeBB. You'll need to set a
|
|
4
|
-
special cookie with a common domain, containing a JSON Web Token with user data. If
|
|
5
|
-
handle the rest (user registration/login).
|
|
4
|
+
special cookie with a common domain, containing a JSON Web Token (signed, or encrypted) with user data. If
|
|
5
|
+
sufficient, this plugin will handle the rest (user registration/login).
|
|
6
6
|
|
|
7
7
|
## How is this related to SSO?
|
|
8
8
|
|
|
@@ -21,8 +21,8 @@ You can use this plugin and single sign-on plugins together, but they won't be s
|
|
|
21
21
|
## How does this work?
|
|
22
22
|
|
|
23
23
|
This plugin checks incoming requests for a **shared cookie** that is saved by your application when a user
|
|
24
|
-
logs in. This cookie contains in its value, a specially crafted signed
|
|
25
|
-
information for that user.
|
|
24
|
+
logs in. This cookie contains in its value, a specially crafted signed (JWS) or encrypted (JWE) token
|
|
25
|
+
containing unique identifying information for that user.
|
|
26
26
|
|
|
27
27
|
If the user can be found in NodeBB, that user will be logged in. If not, then a user is created, and that
|
|
28
28
|
unique indentifier is saved for future reference.
|
|
@@ -36,10 +36,13 @@ to have a couple special attributes:
|
|
|
36
36
|
* The `domain` should be set to the naked domain. That is, if your site is at `app.example.com`, and your
|
|
37
37
|
forum is at `talk.example.com`, the cookie domain should be set to `example.com`.
|
|
38
38
|
* The cookie name is up to you, and can be configured in this plugin's settings. *(Default: `token`)*
|
|
39
|
-
* The cookie value is a [JSON Web Token](https://jwt.io/). See below.
|
|
39
|
+
* The cookie value is a [JSON Web Token](https://jwt.io/) — signed (JWS) or encrypted (JWE). See below.
|
|
40
40
|
|
|
41
41
|
### Generating the JSON Web Token
|
|
42
42
|
|
|
43
|
+
The steps below describe the default **JWS** format; if you've switched the plugin to **JWE**, see
|
|
44
|
+
[Token Format (JWS / JWE)](#token-format-jws--jwe) instead.
|
|
45
|
+
|
|
43
46
|
A list of compatible libraries can be obtained on the main website for [JSON Web Tokens](https://jwt.io/).
|
|
44
47
|
|
|
45
48
|
You'll be encoding a payload that looks something like this...
|
|
@@ -87,14 +90,66 @@ it can properly decode and verify the JWT signature.
|
|
|
87
90
|
|
|
88
91
|
In which case, you can set the "Parent Key" setting in this plugin to `d`.
|
|
89
92
|
|
|
93
|
+
### Token Format (JWS / JWE)
|
|
94
|
+
|
|
95
|
+
By default the shared cookie is a **JWS** (signed) JWT. The **Token Format** setting in the admin panel
|
|
96
|
+
allows switching to **JWE** (encrypted) JWTs, in which case the payload is confidential and only decryptable
|
|
97
|
+
with the corresponding key, so its contents are not visible in the cookie.
|
|
98
|
+
|
|
99
|
+
This setting controls the format of tokens *generated* by NodeBB (the `nbb_token` reverse cookie and the
|
|
100
|
+
development debug token). **Incoming cookies are auto-detected by their structure** (a JWS has 3
|
|
101
|
+
dot-separated segments, a JWE has 5), so *both* formats are accepted regardless of the setting. This allows
|
|
102
|
+
a gradual migration: flip the setting, then switch your application over to issuing JWE cookies at your own
|
|
103
|
+
pace — users with cookies in either format will continue to log in throughout the transition.
|
|
104
|
+
|
|
105
|
+
Tokens are created and verified with [`jose`](https://github.com/panva/jose). When issuing JWE cookies from
|
|
106
|
+
your application, use the following parameters:
|
|
107
|
+
|
|
108
|
+
| Signing method | `alg` | `enc` | Key |
|
|
109
|
+
| --- | --- | --- | --- |
|
|
110
|
+
| HMAC | `dir` | `A256GCM` | HKDF-derived from your shared secret (see below) |
|
|
111
|
+
| RSA | `RSA-OAEP-256` | `A256GCM` | The RSA **public** key |
|
|
112
|
+
|
|
113
|
+
**HMAC key derivation** — JWE's `dir` key method requires the key to be exactly 32 bytes, so
|
|
114
|
+
arbitrary-length shared secrets cannot be used directly. NodeBB derives the JWE key with HKDF-SHA256:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
HKDF(
|
|
118
|
+
ikm: <your shared secret>,
|
|
119
|
+
salt: "nodebb-plugin-session-sharing",
|
|
120
|
+
info: "A256GCM",
|
|
121
|
+
length: 32
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Your application must perform the same derivation before encrypting. JWS is unaffected — `HS256` signing
|
|
126
|
+
uses the raw secret as-is, so existing JWS setups keep working unchanged.
|
|
127
|
+
|
|
128
|
+
**RSA key direction** — note that JWE inverts the key direction of JWS: your application *encrypts* with the
|
|
129
|
+
RSA **public** key, and NodeBB *decrypts* with the RSA **private** key. If you use JWE with the RSA signing
|
|
130
|
+
method, make sure the **RSA Private Key** is configured in the admin settings — without it, encrypted cookies
|
|
131
|
+
cannot be decrypted (a warning is logged at startup).
|
|
132
|
+
|
|
133
|
+
The `nbb_token` reverse cookie follows the same Token Format setting. If your application reads the reverse
|
|
134
|
+
token, it must be able to decrypt JWE when this setting is enabled.
|
|
135
|
+
|
|
90
136
|
## Security
|
|
91
137
|
|
|
92
|
-
Please note that according to the JWT spec,
|
|
138
|
+
Please note that according to the JWT spec, a JWS payload is ***not encrypted***, only *signed*. That is,
|
|
93
139
|
the Base64 Url Encoded payload is appended to the header. It can be decoded trivially (as base64 is not meant
|
|
94
|
-
to be cryptographically secure), so **do not put any private information in the payload
|
|
95
|
-
payload themselves are *signed* against the secret, and NodeBB will only allow a JWT through if it
|
|
96
|
-
tampered with. That is, NodeBB will only continue with a login if the signature can be
|
|
97
|
-
by the received payload and the secret.
|
|
140
|
+
to be cryptographically secure), so **do not put any private information in the payload** of a JWS token. The
|
|
141
|
+
header and payload themselves are *signed* against the secret, and NodeBB will only allow a JWT through if it
|
|
142
|
+
has not been tampered with. That is, NodeBB will only continue with a login if the signature can be
|
|
143
|
+
independently generated by the received payload and the secret.
|
|
144
|
+
|
|
145
|
+
**Mitigating payload exposure with JWE** — if the exposure above is a concern for your setup (for example,
|
|
146
|
+
your payload includes an `email` or `groups` you'd rather not be visible in a cookie, or you cannot guarantee
|
|
147
|
+
HTTPS end-to-end), switch the **Token Format** setting to JWE (see
|
|
148
|
+
[Token Format (JWS / JWE)](#token-format-jws--jwe)). A JWE token *encrypts* the payload — with `A256GCM` in
|
|
149
|
+
this plugin — so the identifying information is not visible in the cookie at all, and can only be decrypted by
|
|
150
|
+
NodeBB with the corresponding key. Note that JWE protects the payload's *confidentiality*; the token is still
|
|
151
|
+
only as trustworthy as the key or secret it was sealed with, and a stolen private key (or shared secret) can
|
|
152
|
+
still mint valid tokens — the rest of this section applies to both formats.
|
|
98
153
|
|
|
99
154
|
Use secure cookies transmitted via HTTPS if at all possible.
|
|
100
155
|
|
package/eslint.config.mjs
CHANGED
package/library.js
CHANGED
|
@@ -4,9 +4,10 @@ const winston = nodebb.require('winston');
|
|
|
4
4
|
const nconf = nodebb.require('nconf');
|
|
5
5
|
|
|
6
6
|
const util = require('util');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
|
|
8
9
|
const _ = require('lodash');
|
|
9
|
-
const
|
|
10
|
+
const { jwtVerify, jwtDecrypt, SignJWT, EncryptJWT } = require('jose');
|
|
10
11
|
|
|
11
12
|
const meta = nodebb.require('./src/meta');
|
|
12
13
|
const user = nodebb.require('./src/user');
|
|
@@ -47,6 +48,10 @@ const plugin = {
|
|
|
47
48
|
cookieName: 'token',
|
|
48
49
|
cookieDomain: undefined,
|
|
49
50
|
secret: '',
|
|
51
|
+
signingMethod: 'hmac',
|
|
52
|
+
tokenFormat: 'jws',
|
|
53
|
+
publicKey: '',
|
|
54
|
+
privateKey: '',
|
|
50
55
|
behaviour: 'trust',
|
|
51
56
|
adminRevalidate: 'off',
|
|
52
57
|
noRegistration: 'off',
|
|
@@ -136,8 +141,93 @@ plugin.getUser = async (remoteId) => {
|
|
|
136
141
|
return user.getUserFields(uid, ['username', 'userslug', 'picture']);
|
|
137
142
|
};
|
|
138
143
|
|
|
144
|
+
/*
|
|
145
|
+
* JWE with a symmetric key ("dir" key method) requires the key to be exactly
|
|
146
|
+
* the length of the content encryption key (32 bytes for A256GCM). Shared
|
|
147
|
+
* secrets are arbitrary-length, so derive a fixed-length key with HKDF.
|
|
148
|
+
* Apps issuing JWE cookies must perform the same derivation.
|
|
149
|
+
*/
|
|
150
|
+
const JWE_HKDF_SALT = 'nodebb-plugin-session-sharing';
|
|
151
|
+
const JWE_HKDF_INFO = 'A256GCM';
|
|
152
|
+
|
|
153
|
+
plugin.getJwsKey = (isPrivate) => {
|
|
154
|
+
if (plugin.settings.signingMethod === 'rsa') {
|
|
155
|
+
const pem = isPrivate ? plugin.settings.privateKey : plugin.settings.publicKey;
|
|
156
|
+
if (!pem) {
|
|
157
|
+
throw new Error(isPrivate ? 'No RSA private key configured' : 'No RSA public key configured');
|
|
158
|
+
}
|
|
159
|
+
// jose v6 expects a KeyObject, not a PEM string
|
|
160
|
+
return isPrivate ? crypto.createPrivateKey(pem) : crypto.createPublicKey(pem);
|
|
161
|
+
}
|
|
162
|
+
if (!plugin.settings.secret) {
|
|
163
|
+
throw new Error('No JWT secret configured');
|
|
164
|
+
}
|
|
165
|
+
return crypto.createSecretKey(plugin.settings.secret, 'utf8');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Note the key direction differs from JWS: JWE is encrypted with the public
|
|
170
|
+
* key and decrypted with the private key, so verifying an incoming JWE
|
|
171
|
+
* cookie requires the RSA private key.
|
|
172
|
+
*/
|
|
173
|
+
plugin.getJweKey = (isDecryption) => {
|
|
174
|
+
if (plugin.settings.signingMethod === 'rsa') {
|
|
175
|
+
return plugin.getJwsKey(isDecryption);
|
|
176
|
+
}
|
|
177
|
+
if (!plugin.settings.secret) {
|
|
178
|
+
throw new Error('No JWT secret configured');
|
|
179
|
+
}
|
|
180
|
+
// jose expects a Uint8Array (not a Node Buffer) for symmetric JWE keys
|
|
181
|
+
return new Uint8Array(crypto.hkdfSync('sha256', plugin.settings.secret, JWE_HKDF_SALT, JWE_HKDF_INFO, 32));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* Verifies an incoming token, accepting both JWS (3 segments) and JWE
|
|
186
|
+
* (5 segments) so that installs can migrate between the two formats
|
|
187
|
+
* without a hard cutover. Key selection is driven by the configured
|
|
188
|
+
* signing method, not the token header.
|
|
189
|
+
*/
|
|
190
|
+
plugin.verifyToken = async (token) => {
|
|
191
|
+
if (typeof token !== 'string' || !token.length) {
|
|
192
|
+
throw new Error('payload-invalid');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isJWE = token.split('.').length === 5;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (isJWE) {
|
|
199
|
+
const { payload } = await jwtDecrypt(token, plugin.getJweKey(true), {
|
|
200
|
+
algorithms: plugin.settings.signingMethod === 'rsa' ? ['RSA-OAEP-256'] : ['dir'],
|
|
201
|
+
});
|
|
202
|
+
return payload; // already JSON-parsed by jose
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { payload } = await jwtVerify(token, plugin.getJwsKey(false), {
|
|
206
|
+
algorithms: plugin.settings.signingMethod === 'rsa' ? ['RS256'] : ['HS256'],
|
|
207
|
+
});
|
|
208
|
+
return payload;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error('payload-invalid', { cause: error });
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/* Creates a JWS or JWE token according to the configured token format */
|
|
215
|
+
plugin.createToken = async (payload) => {
|
|
216
|
+
if (plugin.settings.tokenFormat === 'jwe') {
|
|
217
|
+
const encrypted = new EncryptJWT(payload).setProtectedHeader(
|
|
218
|
+
plugin.settings.signingMethod === 'rsa' ? { alg: 'RSA-OAEP-256', enc: 'A256GCM' } : { alg: 'dir', enc: 'A256GCM' }
|
|
219
|
+
);
|
|
220
|
+
return encrypted.encrypt(plugin.getJweKey(false));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const signed = new SignJWT(payload).setProtectedHeader({
|
|
224
|
+
alg: plugin.settings.signingMethod === 'rsa' ? 'RS256' : 'HS256',
|
|
225
|
+
});
|
|
226
|
+
return signed.sign(plugin.getJwsKey(true));
|
|
227
|
+
};
|
|
228
|
+
|
|
139
229
|
plugin.process = async (token) => {
|
|
140
|
-
const payload = await
|
|
230
|
+
const payload = await plugin.verifyToken(token);
|
|
141
231
|
const userData = await plugin.normalizePayload(payload);
|
|
142
232
|
const [uid, isNewUser] = await plugin.findOrCreateUser(userData);
|
|
143
233
|
await plugin.updateUserProfile(uid, userData, isNewUser);
|
|
@@ -386,10 +476,15 @@ plugin.addMiddleware = async function ({ req, res }) {
|
|
|
386
476
|
const { hostWhitelist, guestRedirect, editOverride, loginOverride, registerOverride } = await meta.settings.get('session-sharing');
|
|
387
477
|
|
|
388
478
|
if (hostWhitelist) {
|
|
479
|
+
const hostHeader = req.headers.host;
|
|
480
|
+
if (!hostHeader) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
389
484
|
const hosts = hostWhitelist.split(',') || [hostWhitelist];
|
|
390
485
|
let whitelisted = false;
|
|
391
486
|
for (const host of hosts) {
|
|
392
|
-
if (
|
|
487
|
+
if (hostHeader.includes(host)) {
|
|
393
488
|
whitelisted = true;
|
|
394
489
|
break;
|
|
395
490
|
}
|
|
@@ -521,13 +616,11 @@ plugin.addMiddleware = async function ({ req, res }) {
|
|
|
521
616
|
};
|
|
522
617
|
|
|
523
618
|
plugin.cleanup = async (data) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
});
|
|
530
|
-
}
|
|
619
|
+
winston.verbose('[session-sharing] Clearing cookie');
|
|
620
|
+
data.res.clearCookie(plugin.settings.cookieName, {
|
|
621
|
+
domain: plugin.settings.cookieDomain,
|
|
622
|
+
path: '/',
|
|
623
|
+
});
|
|
531
624
|
|
|
532
625
|
data.res.clearCookie('nbb_token', {
|
|
533
626
|
domain: plugin.settings.cookieDomain,
|
|
@@ -537,7 +630,7 @@ plugin.cleanup = async (data) => {
|
|
|
537
630
|
return true;
|
|
538
631
|
};
|
|
539
632
|
|
|
540
|
-
plugin.generate = function (req, res) {
|
|
633
|
+
plugin.generate = async function (req, res) {
|
|
541
634
|
if (!plugin.ready) {
|
|
542
635
|
return res.sendStatus(404);
|
|
543
636
|
}
|
|
@@ -563,7 +656,7 @@ plugin.generate = function (req, res) {
|
|
|
563
656
|
payload = newPayload;
|
|
564
657
|
}
|
|
565
658
|
|
|
566
|
-
const token =
|
|
659
|
+
const token = await plugin.createToken(payload);
|
|
567
660
|
res.cookie(plugin.settings.cookieName, token, {
|
|
568
661
|
maxAge: 1000 * 60 * 60 * 24 * 21,
|
|
569
662
|
httpOnly: true,
|
|
@@ -590,10 +683,24 @@ plugin.reloadSettings = async (data) => {
|
|
|
590
683
|
}
|
|
591
684
|
|
|
592
685
|
const settings = await meta.settings.get('session-sharing');
|
|
593
|
-
|
|
686
|
+
const signingMethod = settings.signingMethod || 'hmac';
|
|
687
|
+
const tokenFormat = settings.tokenFormat || 'jws';
|
|
688
|
+
if (tokenFormat !== 'jws' && tokenFormat !== 'jwe') {
|
|
689
|
+
winston.error('[session-sharing] Invalid token format ("' + tokenFormat + '"), session sharing disabled.');
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (signingMethod === 'rsa') {
|
|
693
|
+
if (!settings.publicKey || !settings.publicKey.length) {
|
|
694
|
+
winston.error('[session-sharing] RSA public key not found, session sharing disabled.');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
} else if (!settings.hasOwnProperty('secret') || !settings.secret.length) {
|
|
594
698
|
winston.error('[session-sharing] JWT Secret not found, session sharing disabled.');
|
|
595
699
|
return;
|
|
596
700
|
}
|
|
701
|
+
if (signingMethod === 'rsa' && (!settings.privateKey || !settings.privateKey.length)) {
|
|
702
|
+
winston.warn('[session-sharing] RSA private key not configured; JWE (encrypted) cookies cannot be decrypted and reverse tokens cannot be signed.');
|
|
703
|
+
}
|
|
597
704
|
|
|
598
705
|
// If "payload:parent" is found, but payloadParent is not, update the latter and delete the former
|
|
599
706
|
if (!settings.payloadParent && settings['payload:parent']) {
|
|
@@ -636,7 +743,7 @@ plugin.saveReverseToken = async ({ req, userData: data }) => {
|
|
|
636
743
|
const { res } = req;
|
|
637
744
|
const userData = await user.getUserFields(data.uid, ['uid', 'username', 'picture', 'reputation', 'postcount', 'banned']);
|
|
638
745
|
userData.groups = (await groups.getUserGroups([data.uid])).pop();
|
|
639
|
-
const token =
|
|
746
|
+
const token = await plugin.createToken(userData);
|
|
640
747
|
|
|
641
748
|
res.cookie('nbb_token', token, {
|
|
642
749
|
maxAge: meta.getSessionTTLSeconds() * 1000,
|
package/package-lock.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-session-sharing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "nodebb-plugin-session-sharing",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "9.0.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@commitlint/cli": "^9.1.2",
|
|
13
13
|
"async": "^3",
|
|
14
|
-
"
|
|
14
|
+
"jose": "^6.0.0",
|
|
15
15
|
"lint-staged": "^10.0.9",
|
|
16
16
|
"lodash": "^4.17.14"
|
|
17
17
|
},
|
|
@@ -749,11 +749,6 @@
|
|
|
749
749
|
"node": ">=8"
|
|
750
750
|
}
|
|
751
751
|
},
|
|
752
|
-
"node_modules/buffer-equal-constant-time": {
|
|
753
|
-
"version": "1.0.1",
|
|
754
|
-
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
|
755
|
-
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
|
756
|
-
},
|
|
757
752
|
"node_modules/caller-callsite": {
|
|
758
753
|
"version": "2.0.0",
|
|
759
754
|
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
|
|
@@ -1162,14 +1157,6 @@
|
|
|
1162
1157
|
"safer-buffer": "^2.1.0"
|
|
1163
1158
|
}
|
|
1164
1159
|
},
|
|
1165
|
-
"node_modules/ecdsa-sig-formatter": {
|
|
1166
|
-
"version": "1.0.11",
|
|
1167
|
-
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
|
1168
|
-
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
|
1169
|
-
"dependencies": {
|
|
1170
|
-
"safe-buffer": "^5.0.1"
|
|
1171
|
-
}
|
|
1172
|
-
},
|
|
1173
1160
|
"node_modules/emoji-regex": {
|
|
1174
1161
|
"version": "8.0.0",
|
|
1175
1162
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
@@ -2283,6 +2270,15 @@
|
|
|
2283
2270
|
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
|
|
2284
2271
|
"dev": true
|
|
2285
2272
|
},
|
|
2273
|
+
"node_modules/jose": {
|
|
2274
|
+
"version": "6.2.12",
|
|
2275
|
+
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
|
|
2276
|
+
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
|
|
2277
|
+
"license": "MIT",
|
|
2278
|
+
"funding": {
|
|
2279
|
+
"url": "https://github.com/sponsors/panva"
|
|
2280
|
+
}
|
|
2281
|
+
},
|
|
2286
2282
|
"node_modules/js-tokens": {
|
|
2287
2283
|
"version": "4.0.0",
|
|
2288
2284
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
@@ -2371,35 +2367,6 @@
|
|
|
2371
2367
|
"node": "*"
|
|
2372
2368
|
}
|
|
2373
2369
|
},
|
|
2374
|
-
"node_modules/jsonwebtoken": {
|
|
2375
|
-
"version": "8.5.1",
|
|
2376
|
-
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
|
2377
|
-
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
|
2378
|
-
"dependencies": {
|
|
2379
|
-
"jws": "^3.2.2",
|
|
2380
|
-
"lodash.includes": "^4.3.0",
|
|
2381
|
-
"lodash.isboolean": "^3.0.3",
|
|
2382
|
-
"lodash.isinteger": "^4.0.4",
|
|
2383
|
-
"lodash.isnumber": "^3.0.3",
|
|
2384
|
-
"lodash.isplainobject": "^4.0.6",
|
|
2385
|
-
"lodash.isstring": "^4.0.1",
|
|
2386
|
-
"lodash.once": "^4.0.0",
|
|
2387
|
-
"ms": "^2.1.1",
|
|
2388
|
-
"semver": "^5.6.0"
|
|
2389
|
-
},
|
|
2390
|
-
"engines": {
|
|
2391
|
-
"node": ">=4",
|
|
2392
|
-
"npm": ">=1.4.28"
|
|
2393
|
-
}
|
|
2394
|
-
},
|
|
2395
|
-
"node_modules/jsonwebtoken/node_modules/semver": {
|
|
2396
|
-
"version": "5.7.2",
|
|
2397
|
-
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
|
2398
|
-
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
|
2399
|
-
"bin": {
|
|
2400
|
-
"semver": "bin/semver"
|
|
2401
|
-
}
|
|
2402
|
-
},
|
|
2403
2370
|
"node_modules/jsprim": {
|
|
2404
2371
|
"version": "1.4.2",
|
|
2405
2372
|
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
|
|
@@ -2415,25 +2382,6 @@
|
|
|
2415
2382
|
"node": ">=0.6.0"
|
|
2416
2383
|
}
|
|
2417
2384
|
},
|
|
2418
|
-
"node_modules/jwa": {
|
|
2419
|
-
"version": "1.4.2",
|
|
2420
|
-
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
|
2421
|
-
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
|
2422
|
-
"dependencies": {
|
|
2423
|
-
"buffer-equal-constant-time": "^1.0.1",
|
|
2424
|
-
"ecdsa-sig-formatter": "1.0.11",
|
|
2425
|
-
"safe-buffer": "^5.0.1"
|
|
2426
|
-
}
|
|
2427
|
-
},
|
|
2428
|
-
"node_modules/jws": {
|
|
2429
|
-
"version": "3.2.2",
|
|
2430
|
-
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
|
2431
|
-
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
|
2432
|
-
"dependencies": {
|
|
2433
|
-
"jwa": "^1.4.1",
|
|
2434
|
-
"safe-buffer": "^5.0.1"
|
|
2435
|
-
}
|
|
2436
|
-
},
|
|
2437
2385
|
"node_modules/keyv": {
|
|
2438
2386
|
"version": "4.5.4",
|
|
2439
2387
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
@@ -2622,41 +2570,6 @@
|
|
|
2622
2570
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
2623
2571
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
|
2624
2572
|
},
|
|
2625
|
-
"node_modules/lodash.includes": {
|
|
2626
|
-
"version": "4.3.0",
|
|
2627
|
-
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
|
2628
|
-
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
|
2629
|
-
},
|
|
2630
|
-
"node_modules/lodash.isboolean": {
|
|
2631
|
-
"version": "3.0.3",
|
|
2632
|
-
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
|
2633
|
-
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
|
2634
|
-
},
|
|
2635
|
-
"node_modules/lodash.isinteger": {
|
|
2636
|
-
"version": "4.0.4",
|
|
2637
|
-
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
|
2638
|
-
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
|
2639
|
-
},
|
|
2640
|
-
"node_modules/lodash.isnumber": {
|
|
2641
|
-
"version": "3.0.3",
|
|
2642
|
-
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
|
2643
|
-
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
|
2644
|
-
},
|
|
2645
|
-
"node_modules/lodash.isplainobject": {
|
|
2646
|
-
"version": "4.0.6",
|
|
2647
|
-
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
|
2648
|
-
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
|
2649
|
-
},
|
|
2650
|
-
"node_modules/lodash.isstring": {
|
|
2651
|
-
"version": "4.0.1",
|
|
2652
|
-
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
|
2653
|
-
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
|
2654
|
-
},
|
|
2655
|
-
"node_modules/lodash.once": {
|
|
2656
|
-
"version": "4.1.1",
|
|
2657
|
-
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
|
2658
|
-
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
|
2659
|
-
},
|
|
2660
2573
|
"node_modules/log-symbols": {
|
|
2661
2574
|
"version": "4.1.0",
|
|
2662
2575
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
|
@@ -2852,7 +2765,8 @@
|
|
|
2852
2765
|
"node_modules/ms": {
|
|
2853
2766
|
"version": "2.1.3",
|
|
2854
2767
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
2855
|
-
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
|
2768
|
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
2769
|
+
"dev": true
|
|
2856
2770
|
},
|
|
2857
2771
|
"node_modules/natural-compare": {
|
|
2858
2772
|
"version": "1.4.0",
|
|
@@ -3500,6 +3414,7 @@
|
|
|
3500
3414
|
"version": "5.2.1",
|
|
3501
3415
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
3502
3416
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
3417
|
+
"dev": true,
|
|
3503
3418
|
"funding": [
|
|
3504
3419
|
{
|
|
3505
3420
|
"type": "github",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-session-sharing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Allows login sessions from your app to persist in NodeBB",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@commitlint/cli": "^9.1.2",
|
|
43
43
|
"async": "^3",
|
|
44
|
-
"
|
|
44
|
+
"jose": "^6.0.0",
|
|
45
45
|
"lint-staged": "^10.0.9",
|
|
46
46
|
"lodash": "^4.17.14"
|
|
47
47
|
},
|
package/static/lib/admin.js
CHANGED
|
@@ -4,7 +4,10 @@ define('admin/plugins/session-sharing', ['settings'], function (Settings) {
|
|
|
4
4
|
const ACP = {};
|
|
5
5
|
|
|
6
6
|
ACP.init = function () {
|
|
7
|
-
Settings.load
|
|
7
|
+
// Settings.load is async: only toggle field visibility once the saved
|
|
8
|
+
// signingMethod value has been populated into the form.
|
|
9
|
+
Settings.load('session-sharing', $('.session-sharing-settings'), ACP.toggleSigningFields);
|
|
10
|
+
$('#signingMethod').on('change', ACP.toggleSigningFields);
|
|
8
11
|
|
|
9
12
|
$('#save').on('click', function () {
|
|
10
13
|
Settings.save('session-sharing', $('.session-sharing-settings'));
|
|
@@ -14,6 +17,19 @@ define('admin/plugins/session-sharing', ['settings'], function (Settings) {
|
|
|
14
17
|
$('#remote_search').on('keyup', ACP.findUserByRemoteId);
|
|
15
18
|
};
|
|
16
19
|
|
|
20
|
+
ACP.toggleSigningFields = function () {
|
|
21
|
+
const method = $('#signingMethod').val() || 'hmac';
|
|
22
|
+
if (method === 'rsa') {
|
|
23
|
+
$('#hmac-fields').hide();
|
|
24
|
+
$('#rsa-fields').show();
|
|
25
|
+
$('#rsa-sign-fields').show();
|
|
26
|
+
} else {
|
|
27
|
+
$('#hmac-fields').show();
|
|
28
|
+
$('#rsa-fields').hide();
|
|
29
|
+
$('#rsa-sign-fields').hide();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
17
33
|
ACP.showUserId = function () {
|
|
18
34
|
if (ACP._searchDelay) {
|
|
19
35
|
clearTimeout(ACP._searchDelay);
|
|
@@ -25,6 +25,44 @@
|
|
|
25
25
|
</p>
|
|
26
26
|
</div>
|
|
27
27
|
<div class="mb-3">
|
|
28
|
+
<label class="form-label" for="hostWhitelist">Host Whitelist</label>
|
|
29
|
+
<input type="text" id="hostWhitelist" name="hostWhitelist" title="Host Whitelist" class="form-control" placeholder="localhost" />
|
|
30
|
+
<p class="form-text">
|
|
31
|
+
If set, session-sharing plugin works only on whitelisted domains. Separate with commas, for example: "localhost,test.domain.com".
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<h5 class="fw-bold tracking-tight settings-header">Token Settings</h5>
|
|
36
|
+
|
|
37
|
+
<div class="mb-3">
|
|
38
|
+
<label class="form-label" for="signingMethod">Signing Method</label>
|
|
39
|
+
<select class="form-control" id="signingMethod" name="signingMethod">
|
|
40
|
+
<option value="hmac">HMAC (symmetric shared secret)</option>
|
|
41
|
+
<option value="rsa">RSA (asymmetric key pair)</option>
|
|
42
|
+
</select>
|
|
43
|
+
<p class="form-text">
|
|
44
|
+
HMAC uses a shared secret known to both parties. RSA uses an asymmetric key pair where only the
|
|
45
|
+
issuing server can sign, but any holder of the public key can verify.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="mb-3">
|
|
49
|
+
<label class="form-label" for="tokenFormat">Token Format</label>
|
|
50
|
+
<select class="form-control" id="tokenFormat" name="tokenFormat">
|
|
51
|
+
<option value="jws">JWS (signed JWT)</option>
|
|
52
|
+
<option value="jwe">JWE (encrypted JWT)</option>
|
|
53
|
+
</select>
|
|
54
|
+
<p class="form-text">
|
|
55
|
+
JWS tokens are digitally signed: their payload is base64url-encoded and readable by anyone, but
|
|
56
|
+
tamper-proof. JWE tokens are encrypted: the payload is confidential and only decryptable with the
|
|
57
|
+
corresponding key. This setting controls the format of tokens <em>generated</em> by NodeBB (the
|
|
58
|
+
<code>nbb_token</code> reverse cookie and the development debug token).
|
|
59
|
+
</p>
|
|
60
|
+
<p class="form-text">
|
|
61
|
+
Incoming cookies are auto-detected, so <strong>both</strong> JWS and JWE tokens are accepted
|
|
62
|
+
regardless of this setting.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="mb-3" id="hmac-fields">
|
|
28
66
|
<label class="form-label" for="secret">JWT Secret</label>
|
|
29
67
|
<input type="text" id="secret" name="secret" title="JWT Secret" class="form-control" />
|
|
30
68
|
<p class="form-text">
|
|
@@ -32,13 +70,20 @@
|
|
|
32
70
|
otherwise the JWT cannot be properly decoded.
|
|
33
71
|
</p>
|
|
34
72
|
</div>
|
|
35
|
-
<div class="mb-3">
|
|
36
|
-
<label class="form-label" for="
|
|
37
|
-
<
|
|
73
|
+
<div class="mb-3" id="rsa-fields" style="display: none;">
|
|
74
|
+
<label class="form-label" for="publicKey">RSA Public Key</label>
|
|
75
|
+
<textarea id="publicKey" name="publicKey" title="RSA Public Key" class="form-control" rows="5" placeholder="-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"></textarea>
|
|
38
76
|
<p class="form-text">
|
|
39
|
-
|
|
77
|
+
The public key used to verify JWT signatures. This key is shared with clients so they can verify tokens.
|
|
40
78
|
</p>
|
|
41
79
|
</div>
|
|
80
|
+
<div class="mb-3" id="rsa-sign-fields" style="display: none;">
|
|
81
|
+
<label class="form-label" for="privateKey">RSA Private Key</label>
|
|
82
|
+
<textarea id="privateKey" name="privateKey" title="RSA Private Key" class="form-control" rows="5" placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"></textarea>
|
|
83
|
+
<p class="form-text">
|
|
84
|
+
The private key used to sign reverse tokens (if enabled), and to decrypt JWE (encrypted) cookies. Keep this secret — only the issuing server should have access to it.
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
42
87
|
</div>
|
|
43
88
|
|
|
44
89
|
<div class="mb-4">
|
package/test/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Don't forget to add this plugin to `test_plugins` array in `config.json`
|
|
7
7
|
|
|
8
8
|
const assert = require('assert');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const url = require('url');
|
|
10
11
|
const util = require('util');
|
|
11
12
|
const request = require('request-promise-native');
|
|
@@ -15,6 +16,17 @@ const db = nodebb.require('./test/mocks/databasemock');
|
|
|
15
16
|
const nconf = nodebb.require('nconf');
|
|
16
17
|
const meta = nodebb.require('./src/meta');
|
|
17
18
|
|
|
19
|
+
/*
|
|
20
|
+
* meta.settings.setOne fires the `action:settings.set` hook without awaiting it,
|
|
21
|
+
* so the plugin's reloadSettings may still be running when setOne resolves.
|
|
22
|
+
* Awaiting reloadSettings directly keeps these tests deterministic.
|
|
23
|
+
* (Required lazily to avoid a circular dependency while mocha loads the suite.)
|
|
24
|
+
*/
|
|
25
|
+
const setSetting = async (key, value) => {
|
|
26
|
+
await meta.settings.setOne('session-sharing', key, value);
|
|
27
|
+
await require('../library').reloadSettings();
|
|
28
|
+
};
|
|
29
|
+
|
|
18
30
|
describe('nodebb-plugin-session-sharing', () => {
|
|
19
31
|
const userJar = request.jar();
|
|
20
32
|
const anonJar = request.jar();
|
|
@@ -30,7 +42,7 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
30
42
|
|
|
31
43
|
assert.strictEqual(response.statusCode, 404);
|
|
32
44
|
|
|
33
|
-
await
|
|
45
|
+
await setSetting('secret', 's3cr37c47');
|
|
34
46
|
});
|
|
35
47
|
|
|
36
48
|
it('should generate a valid session when called', async () => {
|
|
@@ -76,12 +88,21 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
76
88
|
});
|
|
77
89
|
assert.strictEqual(response.statusCode, 200);
|
|
78
90
|
|
|
91
|
+
// Core now initialises the guest session (CSRF token), so assert on the
|
|
92
|
+
// session contents: the guest must not have been logged in.
|
|
93
|
+
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
79
94
|
const cookies = anonJar.getCookies(`${protocol}//${hostname}/`);
|
|
80
|
-
|
|
95
|
+
const sidCookie = cookies.find(cookie => cookie.key === 'express.sid');
|
|
96
|
+
if (sidCookie) {
|
|
97
|
+
let sid = sidCookie.value.match(/s%3A([^.]+)/);
|
|
98
|
+
assert(sid && sid[1]);
|
|
99
|
+
const sessionObj = await getSession(sid[1]);
|
|
100
|
+
assert(!sessionObj || !sessionObj.passport || !sessionObj.passport.user);
|
|
101
|
+
}
|
|
81
102
|
});
|
|
82
103
|
|
|
83
104
|
it('should redirect a guest to a specified redirection target if configured', async () => {
|
|
84
|
-
await
|
|
105
|
+
await setSetting('guestRedirect', 'https://example.org');
|
|
85
106
|
const response = await request(`${nconf.get('url')}`, {
|
|
86
107
|
resolveWithFullResponse: true,
|
|
87
108
|
jar: anonJar,
|
|
@@ -91,12 +112,12 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
91
112
|
|
|
92
113
|
assert(response.statusCode, 302);
|
|
93
114
|
assert.strictEqual(response.headers.location, 'https://example.org');
|
|
94
|
-
await
|
|
115
|
+
await setSetting('guestRedirect', '');
|
|
95
116
|
});
|
|
96
117
|
|
|
97
118
|
it('should maintain the login if behaviour is "revalidate"', async () => {
|
|
98
119
|
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
99
|
-
await
|
|
120
|
+
await setSetting('behaviour', 'revalidate');
|
|
100
121
|
|
|
101
122
|
await request(`${nconf.get('url')}`, {
|
|
102
123
|
resolveWithFullResponse: true,
|
|
@@ -152,7 +173,7 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
152
173
|
|
|
153
174
|
describe('login override', () => {
|
|
154
175
|
it('should redirect a guest to a specified login override if configured', async () => {
|
|
155
|
-
await
|
|
176
|
+
await setSetting('loginOverride', 'https://example.org/login');
|
|
156
177
|
const response = await request(`${nconf.get('url')}/login`, {
|
|
157
178
|
resolveWithFullResponse: true,
|
|
158
179
|
jar: anonJar,
|
|
@@ -167,7 +188,7 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
167
188
|
|
|
168
189
|
describe('register override', () => {
|
|
169
190
|
it('should redirect a guest to a specified register override if configured', async () => {
|
|
170
|
-
await
|
|
191
|
+
await setSetting('registerOverride', 'https://example.org/register');
|
|
171
192
|
const response = await request(`${nconf.get('url')}/register`, {
|
|
172
193
|
resolveWithFullResponse: true,
|
|
173
194
|
jar: anonJar,
|
|
@@ -179,4 +200,93 @@ describe('nodebb-plugin-session-sharing', () => {
|
|
|
179
200
|
assert.strictEqual(response.headers.location, 'https://example.org/register');
|
|
180
201
|
});
|
|
181
202
|
});
|
|
203
|
+
|
|
204
|
+
describe('JWE token support', () => {
|
|
205
|
+
const getTokenCookie = (jar) => {
|
|
206
|
+
const cookies = jar.getCookies(`${protocol}//${hostname}/`);
|
|
207
|
+
return cookies.find(cookie => cookie.key === 'token');
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const assertLoggedIn = async (jar) => {
|
|
211
|
+
const getSession = util.promisify(db.sessionStore.get.bind(db.sessionStore));
|
|
212
|
+
const response = await request(`${nconf.get('url')}`, {
|
|
213
|
+
resolveWithFullResponse: true,
|
|
214
|
+
jar,
|
|
215
|
+
});
|
|
216
|
+
assert.strictEqual(response.statusCode, 200);
|
|
217
|
+
|
|
218
|
+
const cookies = jar.getCookies(`${protocol}//${hostname}/`);
|
|
219
|
+
const sidCookie = cookies.find(cookie => cookie.key === 'express.sid');
|
|
220
|
+
assert(sidCookie);
|
|
221
|
+
const sid = sidCookie.value.match(/s%3A([^.]+)/)[1];
|
|
222
|
+
const sessionObj = await getSession(sid);
|
|
223
|
+
|
|
224
|
+
assert(sessionObj && sessionObj.passport && sessionObj.passport.user);
|
|
225
|
+
assert(parseInt(sessionObj.passport.user, 10) > 0);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
it('should generate and accept an HMAC JWE token', async () => {
|
|
229
|
+
const jweJar = request.jar();
|
|
230
|
+
await setSetting('tokenFormat', 'jwe');
|
|
231
|
+
|
|
232
|
+
await request(`${nconf.get('url')}/debug/session`, { jar: jweJar });
|
|
233
|
+
const cookie = getTokenCookie(jweJar);
|
|
234
|
+
assert(cookie);
|
|
235
|
+
assert.strictEqual(cookie.value.split('.').length, 5);
|
|
236
|
+
|
|
237
|
+
await assertLoggedIn(jweJar);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should accept a JWS token even when tokenFormat is jwe (auto-detection)', async () => {
|
|
241
|
+
const jwsJar = request.jar();
|
|
242
|
+
await setSetting('tokenFormat', 'jws');
|
|
243
|
+
|
|
244
|
+
await request(`${nconf.get('url')}/debug/session`, { jar: jwsJar });
|
|
245
|
+
const cookie = getTokenCookie(jwsJar);
|
|
246
|
+
assert(cookie);
|
|
247
|
+
assert.strictEqual(cookie.value.split('.').length, 3);
|
|
248
|
+
|
|
249
|
+
await setSetting('tokenFormat', 'jwe');
|
|
250
|
+
await assertLoggedIn(jwsJar);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should generate and accept an RSA JWE token', async () => {
|
|
254
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
255
|
+
await setSetting('publicKey', publicKey.export({ type: 'spki', format: 'pem' }));
|
|
256
|
+
await setSetting('privateKey', privateKey.export({ type: 'pkcs8', format: 'pem' }));
|
|
257
|
+
await setSetting('signingMethod', 'rsa');
|
|
258
|
+
await setSetting('tokenFormat', 'jwe');
|
|
259
|
+
|
|
260
|
+
const rsaJar = request.jar();
|
|
261
|
+
await request(`${nconf.get('url')}/debug/session`, { jar: rsaJar });
|
|
262
|
+
const cookie = getTokenCookie(rsaJar);
|
|
263
|
+
assert(cookie);
|
|
264
|
+
assert.strictEqual(cookie.value.split('.').length, 5);
|
|
265
|
+
|
|
266
|
+
await assertLoggedIn(rsaJar);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should reject a JWE token decrypted with the wrong key', async () => {
|
|
270
|
+
const { EncryptJWT } = require('jose');
|
|
271
|
+
const wrongKey = new Uint8Array(crypto.hkdfSync('sha256', 'wrong-secret', 'nodebb-plugin-session-sharing', 'A256GCM', 32));
|
|
272
|
+
const token = await new EncryptJWT({ id: 99999, username: 'intruder' })
|
|
273
|
+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
|
274
|
+
.encrypt(wrongKey);
|
|
275
|
+
|
|
276
|
+
const badJar = request.jar();
|
|
277
|
+
badJar.setCookie(`token=${token}`, `${protocol}//${hostname}/`);
|
|
278
|
+
const response = await request(`${nconf.get('url')}`, {
|
|
279
|
+
resolveWithFullResponse: true,
|
|
280
|
+
jar: badJar,
|
|
281
|
+
simple: false,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
assert.strictEqual(response.statusCode, 500);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should restore default signing settings', async () => {
|
|
288
|
+
await setSetting('signingMethod', 'hmac');
|
|
289
|
+
await setSetting('tokenFormat', 'jws');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
182
292
|
});
|