ep_openid_connect 2.5.17 → 3.0.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/CHANGELOG.md +14 -0
- package/README.md +97 -22
- package/ep.json +7 -11
- package/index.js +55 -54
- package/package.json +5 -4
- package/static/tests/backend/oidc-provider.js +4 -0
- package/static/tests/backend/specs/tests.js +174 -7
- package/static/js/client.js +0 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v3.0.0
|
|
4
|
+
|
|
5
|
+
Released 2022-03-04.
|
|
6
|
+
|
|
7
|
+
#### Compatibility changes
|
|
8
|
+
|
|
9
|
+
* Removed the deprecated `displayname_claim` setting. Set
|
|
10
|
+
`user_properties.displayname.claim` instead.
|
|
11
|
+
* Displayname rendering was moved to the
|
|
12
|
+
[ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
|
|
13
|
+
plugin and the `permit_displayname_change` setting was removed.
|
|
14
|
+
* The plugin is now much more strict about what it accepts as valid settings.
|
package/README.md
CHANGED
|
@@ -52,18 +52,12 @@ Etherpad's `requireAuthentication` setting must be `true`.
|
|
|
52
52
|
* `base_url` (required): The public base Etherpad URL. When registering Etherpad
|
|
53
53
|
with your identity provider, the redirect URL (a.k.a. callback URL) is this
|
|
54
54
|
base URL plus `/ep_openid_connect/callback`.
|
|
55
|
-
* `displayname_claim` (optional; defaults to `"name"`): The default value of
|
|
56
|
-
`user_properties.displayname.claim`. **Deprecated:** Set
|
|
57
|
-
`user_properties.displayname.claim` instead.
|
|
58
55
|
* `scope` (optional; defaults to `["openid"]`): List of OAuth2 scope strings.
|
|
59
|
-
* `
|
|
60
|
-
change their displayed name.
|
|
61
|
-
* `prohibited_usernames` (optional; defaults to `['admin', 'guest']`): List of
|
|
56
|
+
* `prohibited_usernames` (optional; defaults to `["admin", "guest"]`): List of
|
|
62
57
|
strings that will trigger an authentication error if any match the `sub`
|
|
63
58
|
(subject) claim from the identity provider. Use this to avoid conflicts with
|
|
64
59
|
the users in the `users` setting and to avoid conflicts with other plugins
|
|
65
|
-
(such as
|
|
66
|
-
[ep\_readonly\_guest](https://github.com/ether/ep_readonly_guest#readme)).
|
|
60
|
+
(such as [ep\_guest](https://github.com/ether/ep_guest#readme)).
|
|
67
61
|
* `user_properties` (optional): Object that controls the automatic creation of
|
|
68
62
|
additional properties on each authenticated user's account object. See below
|
|
69
63
|
for details.
|
|
@@ -79,10 +73,11 @@ will probably look something like "5374".
|
|
|
79
73
|
Each authenticated user gets their own account object. Default properties for a
|
|
80
74
|
user's account object come from the `users` setting in `settings.json`. Etherpad
|
|
81
75
|
uses the `is_admin`, `readOnly`, and `canCreate` properties to control access,
|
|
82
|
-
and
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
and the
|
|
77
|
+
[ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
|
|
78
|
+
plugin uses the `displayname` property for the name displayed in the user list.
|
|
79
|
+
For example, the following sets the default display name to "Firstname Lastname"
|
|
80
|
+
and the default access to read-only for the user identified by "5374":
|
|
86
81
|
|
|
87
82
|
```json
|
|
88
83
|
"users": {
|
|
@@ -98,10 +93,57 @@ you can use the `prohibited_usernames` settings to force an authentication error
|
|
|
98
93
|
if the `sub` claim happens to match. This is useful for preventing a malicious
|
|
99
94
|
identity provider from gaining admin access to your Etherpad instance.
|
|
100
95
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`user_properties` setting
|
|
104
|
-
|
|
96
|
+
### Controlling user account object properties with `user_properties`
|
|
97
|
+
|
|
98
|
+
The `user_properties` setting can be used to automatically add, remove, or
|
|
99
|
+
change properties on a user's account object when the user authenticates. The
|
|
100
|
+
`user_properties` setting maps a property name to a descriptor object that
|
|
101
|
+
describes how the property's value is obtained:
|
|
102
|
+
|
|
103
|
+
* If the descriptor object has a `claim` property that names an existing
|
|
104
|
+
OpenID Connect claim, the value is set to the value of the claim. (If there
|
|
105
|
+
is no such claim, `claim` has no effect.)
|
|
106
|
+
* If the descriptor object has a `default` property and the account object
|
|
107
|
+
property would otherwise be unset, the property is set to the given value.
|
|
108
|
+
(Note that a property set to `undefined` is not the same as unset.)
|
|
109
|
+
* If the descriptor object is `null`, the property is removed if present.
|
|
110
|
+
|
|
111
|
+
Furthermore:
|
|
112
|
+
|
|
113
|
+
* If `user_properties` does not specifiy a descriptor for `displayname`, one
|
|
114
|
+
is added as follows:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"ep_openid_connect": {
|
|
118
|
+
"user_properties": {
|
|
119
|
+
"displayname": {"claim": "name"}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
You can cancel out this default behavior by explicitly specifying an empty
|
|
125
|
+
object:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"ep_openid_connect": {
|
|
129
|
+
"user_properties": {
|
|
130
|
+
"displayname": {}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
* The `username` property is described as follows and cannot be overridden or
|
|
136
|
+
canceled:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
"ep_openid_connect": {
|
|
140
|
+
"user_properties": {
|
|
141
|
+
"username": {"claim": "sub"}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Example:
|
|
105
147
|
|
|
106
148
|
```json
|
|
107
149
|
"ep_openid_connect": {
|
|
@@ -115,7 +157,8 @@ the property's value is obtained. For example:
|
|
|
115
157
|
},
|
|
116
158
|
"fixedValue": {
|
|
117
159
|
"default": "fixed value"
|
|
118
|
-
}
|
|
160
|
+
},
|
|
161
|
+
"forcedUnset": null
|
|
119
162
|
}
|
|
120
163
|
},
|
|
121
164
|
```
|
|
@@ -128,6 +171,10 @@ The above example sets properties as follows:
|
|
|
128
171
|
`claimName` claim if present, otherwise the property is left unset/unchanged.
|
|
129
172
|
* Each user's `fixedValue` property is set to the string `"fixed value"`
|
|
130
173
|
unless already set.
|
|
174
|
+
* Each user's `forcedUnset` property is always deleted if present.
|
|
175
|
+
* Each user's `displayname` property is set to to the value of the user's `name`
|
|
176
|
+
claim if present, otherwise the property is left unset/unchanged.
|
|
177
|
+
* Each user's `username` property is set to the value of the `sub` claim.
|
|
131
178
|
|
|
132
179
|
You can use this feature to control access in the OpenID Connect provider if it
|
|
133
180
|
provides suitable claims:
|
|
@@ -146,15 +193,43 @@ provides suitable claims:
|
|
|
146
193
|
To avoid breaking assumptions made by Etherpad, the `username` property cannot
|
|
147
194
|
be altered via the `user_properties` setting.
|
|
148
195
|
|
|
149
|
-
## Interaction with the ep\
|
|
196
|
+
## Interaction with the ep\_guest Plugin
|
|
150
197
|
|
|
151
|
-
The [ep\
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
for the guest user to the `prohibited_usernames` setting. If the identity
|
|
198
|
+
The [ep\_guest](https://github.com/ether/ep_guest#readme) plugin creates a user
|
|
199
|
+
that is used for all guest accesses. It is recommended you add the username you
|
|
200
|
+
chose for the guest user to the `prohibited_usernames` setting. If the identity
|
|
155
201
|
provider ever uses that username in the `sub` claim, you will get an obvious
|
|
156
202
|
error instead of a mysterious inability to edit pads.
|
|
157
203
|
|
|
204
|
+
## Interaction with the ep\_user\_displayname Plugin
|
|
205
|
+
|
|
206
|
+
By default, this plugin sets the user's `displayname` property to the value of
|
|
207
|
+
the `name` claim. The
|
|
208
|
+
[ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
|
|
209
|
+
plugin uses this property (and the `displaynameChangeable` property) to control
|
|
210
|
+
the name displayed in the pad's list of users.
|
|
211
|
+
|
|
212
|
+
You can change the claim used to get the displayname:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
"ep_openid_connect": {
|
|
216
|
+
"user_properties": {
|
|
217
|
+
"displayname": {"claim": "nickname"}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Or you can cancel the default behavior:
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
"ep_openid_connect": {
|
|
227
|
+
"user_properties": {
|
|
228
|
+
"displayname": {}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
```
|
|
232
|
+
|
|
158
233
|
## Interaction with Etherpad's Built-in HTTP Basic Authentication
|
|
159
234
|
|
|
160
235
|
If the user has not yet successfully authenticated, this plugin defers the
|
package/ep.json
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"parts": [
|
|
3
3
|
{
|
|
4
|
-
"name": "
|
|
5
|
-
"client_hooks": {
|
|
6
|
-
"postToolbarInit": "ep_openid_connect/static/js/client"
|
|
7
|
-
},
|
|
4
|
+
"name": "main",
|
|
8
5
|
"hooks": {
|
|
9
|
-
"authenticate": "
|
|
10
|
-
"authnFailure": "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"preAuthorize": "ep_openid_connect"
|
|
6
|
+
"authenticate": "",
|
|
7
|
+
"authnFailure": "",
|
|
8
|
+
"expressCreateServer": "",
|
|
9
|
+
"init_ep_openid_connect": "",
|
|
10
|
+
"loadSettings": "",
|
|
11
|
+
"preAuthorize": ""
|
|
16
12
|
}
|
|
17
13
|
}
|
|
18
14
|
]
|
package/index.js
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const Ajv = require('ajv/dist/jtd');
|
|
4
4
|
const {URL} = require('url');
|
|
5
5
|
const {Issuer, generators} = require('openid-client');
|
|
6
|
-
const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
let logger = {};
|
|
8
|
+
for (const level of ['debug', 'info', 'warn', 'error']) {
|
|
9
|
+
logger[level] = console[level].bind(console, 'ep_openid_connect:');
|
|
10
|
+
}
|
|
9
11
|
const defaultSettings = {
|
|
10
|
-
displayname_claim: 'name',
|
|
11
|
-
permit_displayname_change: false,
|
|
12
12
|
prohibited_usernames: ['admin', 'guest'],
|
|
13
13
|
scope: ['openid'],
|
|
14
14
|
user_properties: {},
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
let settings;
|
|
17
17
|
let oidcClient = null;
|
|
18
18
|
|
|
19
|
+
const validSettings = new Ajv().compile({
|
|
20
|
+
properties: {
|
|
21
|
+
base_url: {type: 'string'},
|
|
22
|
+
client_id: {type: 'string'},
|
|
23
|
+
client_secret: {type: 'string'},
|
|
24
|
+
},
|
|
25
|
+
optionalProperties: {
|
|
26
|
+
issuer: {type: 'string'},
|
|
27
|
+
issuer_metadata: {},
|
|
28
|
+
prohibited_usernames: {elements: {type: 'string'}},
|
|
29
|
+
scope: {elements: {type: 'string'}},
|
|
30
|
+
user_properties: {values: {
|
|
31
|
+
optionalProperties: {
|
|
32
|
+
claim: {type: 'string'},
|
|
33
|
+
default: {type: 'string'},
|
|
34
|
+
},
|
|
35
|
+
nullable: true,
|
|
36
|
+
}},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
19
40
|
const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
|
|
20
41
|
const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
|
|
21
42
|
|
|
@@ -64,35 +85,37 @@ const getIssuer = async (settings) => {
|
|
|
64
85
|
return new Issuer(settings.issuer_metadata);
|
|
65
86
|
};
|
|
66
87
|
|
|
67
|
-
exports.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
logger.error(
|
|
77
|
-
'Either ep_openid_connect.issuer or ep_openid_connect.issuer_metadata must be set');
|
|
88
|
+
exports.init_ep_openid_connect = async (hookName, {logger: l}) => {
|
|
89
|
+
if (l != null) logger = l;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
exports.loadSettings = async (hookName, {settings: {ep_openid_connect: s = {}}}) => {
|
|
93
|
+
oidcClient = null;
|
|
94
|
+
settings = null;
|
|
95
|
+
if (!validSettings(s)) {
|
|
96
|
+
logger.error('Invalid settings. Detailed validation errors:', validSettings.errors);
|
|
78
97
|
return;
|
|
79
98
|
}
|
|
80
|
-
if (
|
|
81
|
-
logger.
|
|
82
|
-
|
|
99
|
+
if ((s.issuer == null) === (s.issuer_metadata == null)) {
|
|
100
|
+
logger.error('Either ep_openid_connect.issuer or .issuer_metadata must be set (but not both)');
|
|
101
|
+
return;
|
|
83
102
|
}
|
|
84
|
-
if (
|
|
85
|
-
logger.
|
|
86
|
-
|
|
103
|
+
if ('username' in (s.user_properties || {})) {
|
|
104
|
+
logger.error('ep_openid_connect.user_properties.username must not be set');
|
|
105
|
+
return;
|
|
87
106
|
}
|
|
107
|
+
settings = {
|
|
108
|
+
...defaultSettings,
|
|
109
|
+
...s,
|
|
110
|
+
user_properties: {
|
|
111
|
+
displayname: {claim: 'name'},
|
|
112
|
+
...s.user_properties,
|
|
113
|
+
// The username property must always match the key used in settings.users.
|
|
114
|
+
username: {claim: 'sub'},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
88
117
|
// Make sure base_url ends with '/' so that relative URLs are appended:
|
|
89
118
|
if (!settings.base_url.endsWith('/')) settings.base_url += '/';
|
|
90
|
-
settings.user_properties = {
|
|
91
|
-
displayname: {claim: settings.displayname_claim},
|
|
92
|
-
...settings.user_properties,
|
|
93
|
-
// The username property must always match the key used in settings.users.
|
|
94
|
-
username: {claim: 'sub'},
|
|
95
|
-
};
|
|
96
119
|
logger.debug('Settings:', {...settings, client_secret: '********'});
|
|
97
120
|
oidcClient = new (await getIssuer(settings)).Client({
|
|
98
121
|
client_id: settings.client_id,
|
|
@@ -103,12 +126,6 @@ exports.loadSettings = async (hookName, {settings: {ep_openid_connect: config =
|
|
|
103
126
|
logger.info('Configured.');
|
|
104
127
|
};
|
|
105
128
|
|
|
106
|
-
exports.clientVars = (hookName, context) => {
|
|
107
|
-
if (oidcClient == null) return;
|
|
108
|
-
const {permit_displayname_change} = settings;
|
|
109
|
-
return {ep_openid_connect: {permit_displayname_change}};
|
|
110
|
-
};
|
|
111
|
-
|
|
112
129
|
exports.expressCreateServer = (hookName, {app}) => {
|
|
113
130
|
logger.debug('Configuring auth routes');
|
|
114
131
|
app.get(ep('callback'), async (req, res, next) => {
|
|
@@ -194,7 +211,9 @@ exports.authenticate = (hookName, {req, res, users}) => {
|
|
|
194
211
|
req.session.user = users[userinfo.sub];
|
|
195
212
|
if (req.session.user == null) req.session.user = users[userinfo.sub] = {};
|
|
196
213
|
for (const [propName, descriptor] of Object.entries(settings.user_properties)) {
|
|
197
|
-
if (descriptor
|
|
214
|
+
if (descriptor == null) {
|
|
215
|
+
delete req.session.user[propName];
|
|
216
|
+
} else if (descriptor.claim != null && descriptor.claim in userinfo) {
|
|
198
217
|
req.session.user[propName] = userinfo[descriptor.claim];
|
|
199
218
|
} else if ('default' in descriptor && !(propName in req.session.user)) {
|
|
200
219
|
req.session.user[propName] = descriptor.default;
|
|
@@ -244,24 +263,6 @@ exports.authnFailure = (hookName, {req, res}) => {
|
|
|
244
263
|
return true;
|
|
245
264
|
};
|
|
246
265
|
|
|
247
|
-
exports.handleMessage = async (hookName, {message, socket}) => {
|
|
248
|
-
if (oidcClient == null) return;
|
|
249
|
-
logger.debug('handleMessage hook', message);
|
|
250
|
-
const {user: {displayname} = {}} = socket.client.request.session;
|
|
251
|
-
if (!displayname) return;
|
|
252
|
-
if (message.type === 'CLIENT_READY') {
|
|
253
|
-
logger.debug(
|
|
254
|
-
`CLIENT_READY ${socket.id}: Setting username for token ${message.token} to ${displayname}`);
|
|
255
|
-
// TODO: author ID might come from session ID, not token.
|
|
256
|
-
const authorId = await authorManager.getAuthor4Token(message.token);
|
|
257
|
-
await authorManager.setAuthorName(authorId, displayname);
|
|
258
|
-
} else if (message.type === 'COLLABROOM' && message.data.type === 'USERINFO_UPDATE') {
|
|
259
|
-
if (message.data.userInfo.name !== displayname && !settings.permit_displayname_change) {
|
|
260
|
-
message.data.userInfo.name = displayname;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
266
|
exports.preAuthorize = (hookName, {req}) => {
|
|
266
267
|
if (oidcClient == null) return;
|
|
267
268
|
if (req.path.startsWith(ep(''))) return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_openid_connect",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/ether/ep_openid_connect#readme",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
26
|
+
"ajv": "^8.10.0",
|
|
27
|
+
"openid-client": "^5.1.4"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"eslint": "^8.
|
|
30
|
-
"eslint-config-etherpad": "^3.0.
|
|
30
|
+
"eslint": "^8.11.0",
|
|
31
|
+
"eslint-config-etherpad": "^3.0.9",
|
|
31
32
|
"oidc-provider": "^7.10.6",
|
|
32
33
|
"supertest": "^6.2.2",
|
|
33
34
|
"typescript": "^4.6.2"
|
|
@@ -26,6 +26,8 @@ class OidcProvider {
|
|
|
26
26
|
'etherpad_is_admin',
|
|
27
27
|
'etherpad_readOnly',
|
|
28
28
|
'etherpad_canCreate',
|
|
29
|
+
'name',
|
|
30
|
+
'prop',
|
|
29
31
|
],
|
|
30
32
|
},
|
|
31
33
|
cookies: {
|
|
@@ -38,10 +40,12 @@ class OidcProvider {
|
|
|
38
40
|
accountId: sub,
|
|
39
41
|
claims: async (use, scope, claims, rejected) => ({
|
|
40
42
|
sub,
|
|
43
|
+
name: 'Firstname Lastname',
|
|
41
44
|
etherpad_is_admin: sub.includes('admin'),
|
|
42
45
|
etherpad_readOnly: !sub.includes('admin') && sub.includes('readOnly'),
|
|
43
46
|
etherpad_canCreate:
|
|
44
47
|
sub.includes('admin') || (!sub.includes('readOnly') && !sub.includes('noCreate')),
|
|
48
|
+
...(sub === 'claimNull' ? {prop: null} : sub === 'claimVal' ? {prop: 'claimValue'} : {}),
|
|
45
49
|
}),
|
|
46
50
|
}),
|
|
47
51
|
jwks: {
|
|
@@ -6,6 +6,7 @@ const common = require('ep_etherpad-lite/tests/backend/common');
|
|
|
6
6
|
const epOpenidConnect = require('../../../../index');
|
|
7
7
|
const login = require('../login');
|
|
8
8
|
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
|
9
|
+
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
|
|
9
10
|
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
|
10
11
|
const supertest = require('supertest');
|
|
11
12
|
|
|
@@ -33,33 +34,37 @@ describe(__filename, function () {
|
|
|
33
34
|
|
|
34
35
|
before(async function () {
|
|
35
36
|
await common.init();
|
|
37
|
+
if (!plugins.hooks.clientVars) plugins.hooks.clientVars = [];
|
|
38
|
+
backup.hooks = {clientVars: plugins.hooks.clientVars};
|
|
36
39
|
backup.settings = {...settings};
|
|
37
|
-
settings.requireAuthentication = true;
|
|
38
|
-
settings.requireAuthorization = false;
|
|
39
40
|
provider = new OidcProvider();
|
|
40
41
|
const clients =
|
|
41
42
|
[{...client, redirect_uris: [new URL('/ep_openid_connect/callback', common.baseUrl)]}];
|
|
42
43
|
await provider.start({clients});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
beforeEach(async function () {
|
|
47
|
+
agent = supertest.agent('');
|
|
43
48
|
issuer = provider.issuer;
|
|
44
49
|
pluginSettings.base_url = common.baseUrl;
|
|
45
50
|
pluginSettings.issuer = issuer;
|
|
51
|
+
settings.requireAuthentication = true;
|
|
52
|
+
settings.requireAuthorization = false;
|
|
53
|
+
settings.users = {};
|
|
46
54
|
await epOpenidConnect.loadSettings(
|
|
47
55
|
'loadSettings', {settings: {ep_openid_connect: pluginSettings}});
|
|
48
56
|
});
|
|
49
57
|
|
|
50
|
-
beforeEach(async function () {
|
|
51
|
-
agent = supertest.agent('');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
58
|
afterEach(async function () {
|
|
55
59
|
if (socket != null) socket.close();
|
|
56
60
|
socket = null;
|
|
61
|
+
Object.assign(plugins.hooks, backup.hooks);
|
|
62
|
+
Object.assign(settings, backup.settings);
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
after(async function () {
|
|
60
66
|
if (provider != null) await provider.stop();
|
|
61
67
|
provider = null;
|
|
62
|
-
Object.assign(settings, backup.settings);
|
|
63
68
|
});
|
|
64
69
|
|
|
65
70
|
it('not logged in redirects to login endpoint', async function () {
|
|
@@ -225,4 +230,166 @@ describe(__filename, function () {
|
|
|
225
230
|
.expect(401)
|
|
226
231
|
.expect('www-authenticate', 'Etherpad');
|
|
227
232
|
});
|
|
233
|
+
|
|
234
|
+
describe('user_properties', function () {
|
|
235
|
+
it('uses existing settings.users[username] object', async function () {
|
|
236
|
+
const wantUser = {foo: 'bar'};
|
|
237
|
+
settings.users.normalUser = wantUser;
|
|
238
|
+
const padId = common.randomString();
|
|
239
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
240
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
241
|
+
assert.equal(res.request.url, url);
|
|
242
|
+
assert.equal(res.status, 200);
|
|
243
|
+
socket = await common.connect(res);
|
|
244
|
+
const gotUserP = new Promise((resolve) => {
|
|
245
|
+
plugins.hooks.clientVars =
|
|
246
|
+
[{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
|
|
247
|
+
});
|
|
248
|
+
const msg = await common.handshake(socket, padId);
|
|
249
|
+
assert.equal(msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
|
|
250
|
+
assert.equal(settings.users.normalUser, wantUser);
|
|
251
|
+
assert.equal(settings.users.normalUser.foo, 'bar');
|
|
252
|
+
// They won't be reference equal because each request loads the user object from the session
|
|
253
|
+
// store.
|
|
254
|
+
assert.deepEqual(await gotUserP, settings.users.normalUser);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('sets settings.users[username] to new object if missing', async function () {
|
|
258
|
+
assert(settings.users.normalUser == null);
|
|
259
|
+
const padId = common.randomString();
|
|
260
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
261
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
262
|
+
assert.equal(res.request.url, url);
|
|
263
|
+
assert.equal(res.status, 200);
|
|
264
|
+
socket = await common.connect(res);
|
|
265
|
+
const gotUserP = new Promise((resolve) => {
|
|
266
|
+
plugins.hooks.clientVars =
|
|
267
|
+
[{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
|
|
268
|
+
});
|
|
269
|
+
const msg = await common.handshake(socket, padId);
|
|
270
|
+
assert.equal(msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
|
|
271
|
+
assert(settings.users.normalUser != null);
|
|
272
|
+
// They won't be reference equal because each request loads the user object from the session
|
|
273
|
+
// store.
|
|
274
|
+
assert.deepEqual(await gotUserP, settings.users.normalUser);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('displayname defaults to name claim', async function () {
|
|
278
|
+
assert(settings.users.normalUser == null);
|
|
279
|
+
const padId = common.randomString();
|
|
280
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
281
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
282
|
+
assert.equal(res.request.url, url);
|
|
283
|
+
assert.equal(res.status, 200);
|
|
284
|
+
assert.equal(settings.users.normalUser.displayname, 'Firstname Lastname');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('setting empty displayname descriptor cancels default behavior', async function () {
|
|
288
|
+
assert(settings.users.normalUser == null);
|
|
289
|
+
await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
|
|
290
|
+
...pluginSettings,
|
|
291
|
+
user_properties: {displayname: {}},
|
|
292
|
+
}}});
|
|
293
|
+
const padId = common.randomString();
|
|
294
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
295
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
296
|
+
assert.equal(res.request.url, url);
|
|
297
|
+
assert.equal(res.status, 200);
|
|
298
|
+
assert(!('displayname' in settings.users.normalUser));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('username is set to sub claim', async function () {
|
|
302
|
+
assert(settings.users.normalUser == null);
|
|
303
|
+
const padId = common.randomString();
|
|
304
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
305
|
+
const res = await login(agent, issuer, url, 'normalUser');
|
|
306
|
+
assert.equal(res.request.url, url);
|
|
307
|
+
assert.equal(res.status, 200);
|
|
308
|
+
assert.equal(settings.users.normalUser.username, 'normalUser');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('username can\'t be changed', function () {
|
|
312
|
+
for (const dsc of [
|
|
313
|
+
null,
|
|
314
|
+
{},
|
|
315
|
+
{default: 'something else'},
|
|
316
|
+
{claim: 'preferred_username'},
|
|
317
|
+
{claim: 'preferred_username', default: 'something else'},
|
|
318
|
+
]) {
|
|
319
|
+
it(`descriptor: ${JSON.stringify(dsc)}`, async function () {
|
|
320
|
+
await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
|
|
321
|
+
...pluginSettings,
|
|
322
|
+
user_properties: {username: dsc},
|
|
323
|
+
}}});
|
|
324
|
+
// Settings validation should fail, resulting in an unconfigured state and causing
|
|
325
|
+
// Etherpad to fall back to built-in authn.
|
|
326
|
+
const res = await agent.get(new URL('/ep_openid_connect/login', common.baseUrl));
|
|
327
|
+
assert.equal(res.status, 401);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('{settings.json, claim value, user_properties} combinations', function () {
|
|
333
|
+
const testCases = [];
|
|
334
|
+
for (const user of [{}, {prop: undefined}, {prop: null}, {prop: 'userValue'}]) {
|
|
335
|
+
for (const claimValue of [undefined, null, 'claimValue']) {
|
|
336
|
+
for (const cfg of [
|
|
337
|
+
{},
|
|
338
|
+
{prop: null},
|
|
339
|
+
{prop: {}},
|
|
340
|
+
{prop: {default: 'defaultValue'}},
|
|
341
|
+
{prop: {claim: 'prop'}},
|
|
342
|
+
{prop: {claim: 'prop', default: 'defaultValue'}},
|
|
343
|
+
]) {
|
|
344
|
+
const p = cfg.prop || {};
|
|
345
|
+
const wantIn = !!(
|
|
346
|
+
p.default ||
|
|
347
|
+
(p.claim && claimValue !== undefined) ||
|
|
348
|
+
('prop' in user && cfg.prop !== null)); // eslint-disable-line eqeqeq
|
|
349
|
+
const wantValue =
|
|
350
|
+
p.claim && claimValue !== undefined ? claimValue
|
|
351
|
+
: 'prop' in user ? user.prop
|
|
352
|
+
: p.default ? p.default
|
|
353
|
+
: undefined;
|
|
354
|
+
const desc =
|
|
355
|
+
`user.prop=${'prop' in user ? user.prop : '<unset>'}, ` +
|
|
356
|
+
`claimValue=${claimValue === undefined ? '<unset>' : claimValue}, ` +
|
|
357
|
+
`user_properties.prop=${'prop' in cfg ? JSON.stringify(cfg.prop) : '<unset>'} -> ` +
|
|
358
|
+
`${wantIn ? wantValue : '<unset>'}`;
|
|
359
|
+
testCases.push({desc, user, claimValue, cfg, wantIn, wantValue});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const {desc, user, claimValue, cfg, wantIn, wantValue} of testCases) {
|
|
365
|
+
it(desc, async function () {
|
|
366
|
+
const username =
|
|
367
|
+
claimValue === undefined ? 'claimUnset'
|
|
368
|
+
: claimValue != null ? 'claimVal'
|
|
369
|
+
: 'claimNull';
|
|
370
|
+
settings.users[username] = {...user};
|
|
371
|
+
await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
|
|
372
|
+
...pluginSettings,
|
|
373
|
+
user_properties: cfg,
|
|
374
|
+
}}});
|
|
375
|
+
const padId = common.randomString();
|
|
376
|
+
const url = new URL(`/p/${padId}`, common.baseUrl).toString();
|
|
377
|
+
const res = await login(agent, issuer, url, username);
|
|
378
|
+
assert.equal(res.request.url, url);
|
|
379
|
+
assert.equal(res.status, 200);
|
|
380
|
+
socket = await common.connect(res);
|
|
381
|
+
const userP = new Promise((resolve) => {
|
|
382
|
+
plugins.hooks.clientVars =
|
|
383
|
+
[{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
|
|
384
|
+
});
|
|
385
|
+
const msg = await common.handshake(socket, padId);
|
|
386
|
+
assert.equal(
|
|
387
|
+
msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
|
|
388
|
+
const gotUser = await userP;
|
|
389
|
+
assert.equal('prop' in gotUser, wantIn);
|
|
390
|
+
if (wantIn) assert.equal(gotUser.prop, wantValue);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
228
395
|
});
|