ep_openid_connect 2.0.6 → 2.1.1
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/.github/workflows/backend-tests.yml +20 -9
- package/.github/workflows/frontend-tests.yml +92 -0
- package/.github/workflows/npmpublish.yml +12 -8
- package/README.md +75 -9
- package/index.js +87 -24
- package/package.json +10 -9
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# You need to change lines 38 and 46 in case the plugin's name on npmjs.com is different
|
|
2
|
-
# from the repository name
|
|
3
|
-
|
|
4
1
|
name: "Backend tests"
|
|
5
2
|
|
|
6
3
|
# any branch is useful for testing before a PR is submitted
|
|
@@ -17,6 +14,10 @@ jobs:
|
|
|
17
14
|
runs-on: ubuntu-latest
|
|
18
15
|
|
|
19
16
|
steps:
|
|
17
|
+
- uses: actions/setup-node@v2
|
|
18
|
+
with:
|
|
19
|
+
node-version: 12
|
|
20
|
+
|
|
20
21
|
- name: Install libreoffice
|
|
21
22
|
run: |
|
|
22
23
|
sudo add-apt-repository -y ppa:libreoffice/ppa
|
|
@@ -32,19 +33,29 @@ jobs:
|
|
|
32
33
|
- name: Install all dependencies and symlink for ep_etherpad-lite
|
|
33
34
|
run: src/bin/installDeps.sh
|
|
34
35
|
|
|
35
|
-
# clone this repository into node_modules/ep_plugin-name
|
|
36
36
|
- name: Checkout plugin repository
|
|
37
37
|
uses: actions/checkout@v2
|
|
38
38
|
with:
|
|
39
|
-
path: ./node_modules
|
|
39
|
+
path: ./node_modules/__tmp
|
|
40
|
+
|
|
41
|
+
- name: Determine plugin name
|
|
42
|
+
id: plugin_name
|
|
43
|
+
run: |
|
|
44
|
+
cd ./node_modules/__tmp
|
|
45
|
+
npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"'
|
|
46
|
+
|
|
47
|
+
- name: Rename plugin directory
|
|
48
|
+
run: |
|
|
49
|
+
mv ./node_modules/__tmp ./node_modules/"${PLUGIN_NAME}"
|
|
50
|
+
env:
|
|
51
|
+
PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
|
|
40
52
|
|
|
41
53
|
- name: Install plugin dependencies
|
|
42
54
|
run: |
|
|
43
|
-
cd node_modules
|
|
55
|
+
cd ./node_modules/"${PLUGIN_NAME}"
|
|
44
56
|
npm ci
|
|
57
|
+
env:
|
|
58
|
+
PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
|
|
45
59
|
|
|
46
60
|
- name: Run the backend tests
|
|
47
61
|
run: cd src && npm test
|
|
48
|
-
|
|
49
|
-
##ETHERPAD_NPM_V=2
|
|
50
|
-
## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Publicly credit Sauce Labs because they generously support open source
|
|
2
|
+
# projects.
|
|
3
|
+
name: "frontend tests powered by Sauce Labs"
|
|
4
|
+
|
|
5
|
+
on: [push]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Generate Sauce Labs strings
|
|
13
|
+
id: sauce_strings
|
|
14
|
+
run: |
|
|
15
|
+
printf %s\\n '::set-output name=name::${{github.event.repository.name}} ${{ github.workflow }} - ${{ github.job }}'
|
|
16
|
+
printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v2
|
|
19
|
+
with:
|
|
20
|
+
node-version: 12
|
|
21
|
+
|
|
22
|
+
- name: Check out Etherpad core
|
|
23
|
+
uses: actions/checkout@v2
|
|
24
|
+
with:
|
|
25
|
+
repository: ether/etherpad-lite
|
|
26
|
+
|
|
27
|
+
- name: Check out the plugin
|
|
28
|
+
uses: actions/checkout@v2
|
|
29
|
+
with:
|
|
30
|
+
path: ./node_modules/__tmp
|
|
31
|
+
|
|
32
|
+
- name: export GIT_HASH to env
|
|
33
|
+
id: environment
|
|
34
|
+
run: |
|
|
35
|
+
cd ./node_modules/__tmp
|
|
36
|
+
echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
|
|
37
|
+
|
|
38
|
+
- name: Determine plugin name
|
|
39
|
+
id: plugin_name
|
|
40
|
+
run: |
|
|
41
|
+
cd ./node_modules/__tmp
|
|
42
|
+
npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"'
|
|
43
|
+
|
|
44
|
+
- name: Rename plugin directory
|
|
45
|
+
env:
|
|
46
|
+
PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
|
|
47
|
+
run: |
|
|
48
|
+
mv ./node_modules/__tmp ./node_modules/"${PLUGIN_NAME}"
|
|
49
|
+
|
|
50
|
+
- name: Install plugin dependencies
|
|
51
|
+
env:
|
|
52
|
+
PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
|
|
53
|
+
run: |
|
|
54
|
+
cd ./node_modules/"${PLUGIN_NAME}"
|
|
55
|
+
npm ci
|
|
56
|
+
|
|
57
|
+
# This must be run after setting up the plugin, otherwise npm will try to
|
|
58
|
+
# hoist common dependencies by removing them from src/node_modules and
|
|
59
|
+
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
|
|
60
|
+
# logic appears to be buggy, because it sometimes removes dependencies from
|
|
61
|
+
# src/node_modules but fails to add them to the top-level node_modules. Even
|
|
62
|
+
# if npm correctly hoists the dependencies, the hoisting seems to confuse
|
|
63
|
+
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
|
|
64
|
+
- name: Install Etherpad core dependencies
|
|
65
|
+
run: src/bin/installDeps.sh
|
|
66
|
+
|
|
67
|
+
- name: Create settings.json
|
|
68
|
+
run: cp settings.json.template settings.json
|
|
69
|
+
|
|
70
|
+
- name: Disable import/export rate limiting
|
|
71
|
+
run: |
|
|
72
|
+
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
|
|
73
|
+
|
|
74
|
+
- name: Remove standard frontend test files
|
|
75
|
+
run: rm -rf src/tests/frontend/specs
|
|
76
|
+
|
|
77
|
+
- uses: saucelabs/sauce-connect-action@v1
|
|
78
|
+
with:
|
|
79
|
+
username: ${{ secrets.SAUCE_USERNAME }}
|
|
80
|
+
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
|
81
|
+
tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
|
|
82
|
+
|
|
83
|
+
- name: Run the frontend tests
|
|
84
|
+
shell: bash
|
|
85
|
+
env:
|
|
86
|
+
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
|
|
87
|
+
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
|
88
|
+
SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
|
|
89
|
+
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
|
|
90
|
+
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
|
|
91
|
+
run: |
|
|
92
|
+
src/tests/frontend/travis/runner.sh
|
|
@@ -56,15 +56,22 @@ jobs:
|
|
|
56
56
|
runs-on: ubuntu-latest
|
|
57
57
|
steps:
|
|
58
58
|
- uses: actions/checkout@v2
|
|
59
|
+
with:
|
|
60
|
+
fetch-depth: 0
|
|
59
61
|
- uses: actions/setup-node@v1
|
|
60
62
|
with:
|
|
61
63
|
node-version: 12
|
|
62
64
|
registry-url: https://registry.npmjs.org/
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
- name: Bump version (patch)
|
|
66
|
+
run: |
|
|
67
|
+
LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1
|
|
68
|
+
NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1
|
|
69
|
+
[ "${NEW_COMMITS}" -gt 0 ] || exit 0
|
|
70
|
+
git config user.name 'github-actions[bot]'
|
|
71
|
+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
|
72
|
+
npm ci
|
|
73
|
+
npm version patch
|
|
74
|
+
git push --follow-tags
|
|
68
75
|
# `npm publish` must come after `git push` otherwise there is a race
|
|
69
76
|
# condition: If two PRs are merged back-to-back then master/main will be
|
|
70
77
|
# updated with the commits from the second PR before the first PR's
|
|
@@ -78,6 +85,3 @@ jobs:
|
|
|
78
85
|
- run: npm publish
|
|
79
86
|
env:
|
|
80
87
|
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
81
|
-
|
|
82
|
-
##ETHERPAD_NPM_V=2
|
|
83
|
-
## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh
|
package/README.md
CHANGED
|
@@ -34,18 +34,30 @@ Etherpad's `requireAuthentication` setting must be `true`.
|
|
|
34
34
|
|
|
35
35
|
### Configuration Details
|
|
36
36
|
|
|
37
|
-
* `issuer` (required): The base URL of the
|
|
38
|
-
used to
|
|
37
|
+
* `issuer` (required if `issuer_metadata` is not set): The base URL of the
|
|
38
|
+
OpenID Connect identity provider, used to discover the relevant OpenID Connect
|
|
39
|
+
endpoints. If set, your identity provider must support the [OpenID Connect
|
|
40
|
+
Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
|
41
|
+
protocol.
|
|
42
|
+
* `issuer_metadata` (required if `issuer` is not set): Object containing details
|
|
43
|
+
about your OpenID Connect identity provider. Used for manual configuration if
|
|
44
|
+
your identity provider does not support the Discovery protocol. If your
|
|
45
|
+
identity provider does support Discovery, you are encouraged to set `issuer`
|
|
46
|
+
instead. For properties, see the [documentation for the openid-client `Issuer`
|
|
47
|
+
object
|
|
48
|
+
constructor](https://github.com/panva/node-openid-client/blob/v4.7.4/docs/README.md#new-issuermetadata).
|
|
39
49
|
* `client_id` (required): The OAuth2 client ID issued by the identity provider.
|
|
40
50
|
* `client_secret` (required): The OAuth2 client secret issued by the identity
|
|
41
51
|
provider.
|
|
42
52
|
* `base_url` (required): The public base Etherpad URL. When registering Etherpad
|
|
43
53
|
with your identity provider, the redirect URL (a.k.a. callback URL) is this
|
|
44
54
|
base URL plus `/ep_openid_connect/callback`.
|
|
45
|
-
* `displayname_claim` (optional; defaults to `"name"`): The
|
|
46
|
-
|
|
55
|
+
* `displayname_claim` (optional; defaults to `"name"`): The default value of
|
|
56
|
+
`user_properties.displayname.claim`. **Deprecated:** Set
|
|
57
|
+
`user_properties.displayname.claim` instead.
|
|
47
58
|
* `response_types` (optional; defaults to `["code"]`): List of OpenID Connect
|
|
48
59
|
response types.
|
|
60
|
+
* `scope` (optional; defaults to `["openid"]`): List of OAuth2 scope strings.
|
|
49
61
|
* `permit_displayname_change` (optional; defaults to `false`): Whether users may
|
|
50
62
|
change their displayed name.
|
|
51
63
|
* `prohibited_usernames` (optional; defaults to `['admin', 'guest']`): List of
|
|
@@ -54,6 +66,9 @@ Etherpad's `requireAuthentication` setting must be `true`.
|
|
|
54
66
|
the users in the `users` setting and to avoid conflicts with other plugins
|
|
55
67
|
(such as
|
|
56
68
|
[ep\_readonly\_guest](https://github.com/ether/ep_readonly_guest#readme)).
|
|
69
|
+
* `user_properties` (optional): Object that controls the automatic creation of
|
|
70
|
+
additional properties on each authenticated user's account object. See below
|
|
71
|
+
for details.
|
|
57
72
|
|
|
58
73
|
## Interaction with the `users` Setting
|
|
59
74
|
|
|
@@ -63,15 +78,18 @@ identifier for the user.) Many identity providers (such as GitLab) identify
|
|
|
63
78
|
users by a numeric user ID, so the `sub` claim (and thus the Etherpad username)
|
|
64
79
|
will probably look something like "5374".
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
Each authenticated user gets their own account object. Default properties for a
|
|
82
|
+
user's account object come from the `users` setting in `settings.json`. Etherpad
|
|
83
|
+
uses the `is_admin`, `readOnly`, and `canCreate` properties to control access,
|
|
84
|
+
and this plugin uses the `displayname` property for the name displayed in the
|
|
85
|
+
user list. For example, the following sets the default display name to
|
|
86
|
+
"Firstname Lastname" and the default access to read-only for the user identified
|
|
87
|
+
by "5374":
|
|
71
88
|
|
|
72
89
|
```json
|
|
73
90
|
"users": {
|
|
74
91
|
"5374": {
|
|
92
|
+
"displayname": "Firstname Lastname",
|
|
75
93
|
"readOnly": true
|
|
76
94
|
}
|
|
77
95
|
},
|
|
@@ -82,6 +100,54 @@ you can use the `prohibited_usernames` settings to force an authentication error
|
|
|
82
100
|
if the `sub` claim happens to match. This is useful for preventing a malicious
|
|
83
101
|
identity provider from gaining admin access to your Etherpad instance.
|
|
84
102
|
|
|
103
|
+
The `user_properties` setting can be used to automatically add or change
|
|
104
|
+
properties on a user's account object when the user authenticates. The
|
|
105
|
+
`user_properties` setting maps a property name to an object that describes how
|
|
106
|
+
the property's value is obtained. For example:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
"ep_openid_connect": {
|
|
110
|
+
"user_properties": {
|
|
111
|
+
"fromClaimWithDefault": {
|
|
112
|
+
"claim": "claimName",
|
|
113
|
+
"default": "default value"
|
|
114
|
+
},
|
|
115
|
+
"fromClaimOrUnset": {
|
|
116
|
+
"claim": "claimName"
|
|
117
|
+
},
|
|
118
|
+
"fixedValue": {
|
|
119
|
+
"default": "fixed value"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The above example sets properties as follows:
|
|
126
|
+
* Each user's `fromClaimWithDefault` property is set to the value of the user's
|
|
127
|
+
`claimName` claim if present, otherwise the property is left unchanged if
|
|
128
|
+
already set, otherwise it is set to the string `"default value"`.
|
|
129
|
+
* Each user's `fromClaimOrUnset` property is set to the value of the user's
|
|
130
|
+
`claimName` claim if present, otherwise the property is left unset/unchanged.
|
|
131
|
+
* Each user's `fixedValue` property is set to the string `"fixed value"`
|
|
132
|
+
unless already set.
|
|
133
|
+
|
|
134
|
+
You can use this feature to control access in the OpenID Connect provider if it
|
|
135
|
+
provides suitable claims:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
"ep_openid_connect": {
|
|
139
|
+
"scope": ["openid", "etherpad"],
|
|
140
|
+
"user_properties": {
|
|
141
|
+
"is_admin": {"claim": "etherpad_is_admin"},
|
|
142
|
+
"readOnly": {"claim": "etherpad_readOnly"},
|
|
143
|
+
"canCreate": {"claim": "etherpad_canCreate"}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
To avoid breaking assumptions made by Etherpad, the `username` property cannot
|
|
149
|
+
be altered via the `user_properties` setting.
|
|
150
|
+
|
|
85
151
|
## Interaction with the ep\_readonly\_guest Plugin
|
|
86
152
|
|
|
87
153
|
The [ep\_readonly\_guest
|
package/index.js
CHANGED
|
@@ -5,42 +5,94 @@ const {URL} = require('url');
|
|
|
5
5
|
const {Issuer, generators} = require('openid-client');
|
|
6
6
|
const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const logger = log4js.getLogger(pluginName);
|
|
8
|
+
const logger = log4js.getLogger('ep_openid_connect');
|
|
10
9
|
const settings = {
|
|
11
10
|
displayname_claim: 'name',
|
|
12
11
|
response_types: ['code'],
|
|
13
12
|
permit_displayname_change: false,
|
|
14
13
|
prohibited_usernames: ['admin', 'guest'],
|
|
14
|
+
scope: ['openid'],
|
|
15
|
+
user_properties: {},
|
|
15
16
|
};
|
|
16
17
|
let oidcClient = null;
|
|
17
18
|
|
|
18
|
-
const ep = (endpoint) =>
|
|
19
|
+
const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
|
|
19
20
|
const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
const discoverIssuer = async (issuerUrl) => {
|
|
23
|
+
issuerUrl = new URL(issuerUrl);
|
|
24
|
+
// https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery says that the URI
|
|
25
|
+
// must not have query or fragment components.
|
|
26
|
+
if (issuerUrl.search) {
|
|
27
|
+
throw new Error(`Unexpected query in issuer URL (${issuerUrl}): ${issuerUrl.search}`);
|
|
28
|
+
}
|
|
29
|
+
if (issuerUrl.hash) {
|
|
30
|
+
throw new Error(`Unexpected fragment in issuer URL (${issuerUrl}): ${issuerUrl.hash}`);
|
|
31
|
+
}
|
|
32
|
+
let issuer;
|
|
33
|
+
try {
|
|
34
|
+
issuer = await Issuer.discover(issuerUrl.href);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// The URL used to get the issuer metadata doesn't exactly follow RFC 8615; see:
|
|
37
|
+
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
|
38
|
+
const discoveryUrl = new URL(issuerUrl);
|
|
39
|
+
if (!discoveryUrl.pathname.includes('/.well-known/')) {
|
|
40
|
+
discoveryUrl.pathname =
|
|
41
|
+
`${discoveryUrl.pathname.replace(/\/$/, '')}/.well-known/openid-configuration`;
|
|
42
|
+
}
|
|
43
|
+
logger.error(
|
|
44
|
+
'Failed to discover issuer metadata via OpenID Connect Discovery ' +
|
|
45
|
+
'(https://openid.net/specs/openid-connect-discovery-1_0.html). ' +
|
|
46
|
+
`Does your issuer support Discovery? (hint: ${discoveryUrl})`);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
logger.info('OpenID Connect Discovery complete.');
|
|
50
|
+
return issuer;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getIssuer = async (settings) => {
|
|
54
|
+
if (settings.issuer) return await discoverIssuer(settings.issuer);
|
|
55
|
+
return new Issuer(settings.issuer_metadata);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
exports.loadSettings = async (hookName, {settings: {ep_openid_connect: config = {}}}) => {
|
|
22
59
|
Object.assign(settings, config);
|
|
23
|
-
for (const setting of ['base_url', 'client_id', 'client_secret'
|
|
60
|
+
for (const setting of ['base_url', 'client_id', 'client_secret']) {
|
|
24
61
|
if (!settings[setting]) {
|
|
25
|
-
logger.error(`Required setting missing from settings.json:
|
|
62
|
+
logger.error(`Required setting missing from settings.json: ep_openid_connect.${setting}`);
|
|
26
63
|
return;
|
|
27
64
|
}
|
|
28
65
|
}
|
|
66
|
+
if (!settings.issuer && !settings.issuer_metadata) {
|
|
67
|
+
logger.error(
|
|
68
|
+
'Either ep_openid_connect.issuer or ep_openid_connect.issuer_metadata must be set');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (settings.issuer && settings.issuer_metadata) {
|
|
72
|
+
logger.warn('Ignoring ep_openid_connect.issuer_metadata setting ' +
|
|
73
|
+
'because ep_openid_connect.issuer is set');
|
|
74
|
+
}
|
|
29
75
|
// Make sure base_url ends with '/' so that relative URLs are appended:
|
|
30
76
|
if (!settings.base_url.endsWith('/')) settings.base_url += '/';
|
|
31
|
-
|
|
77
|
+
settings.user_properties = {
|
|
78
|
+
displayname: {claim: settings.displayname_claim},
|
|
79
|
+
...settings.user_properties,
|
|
80
|
+
// The username property must always match the key used in settings.users.
|
|
81
|
+
username: {claim: 'sub'},
|
|
82
|
+
};
|
|
83
|
+
oidcClient = new (await getIssuer(settings)).Client({
|
|
32
84
|
client_id: settings.client_id,
|
|
33
85
|
client_secret: settings.client_secret,
|
|
34
86
|
response_types: settings.response_types,
|
|
35
87
|
redirect_uris: [endpointUrl('callback')],
|
|
36
88
|
});
|
|
37
|
-
logger.info('
|
|
89
|
+
logger.info('Configured.');
|
|
38
90
|
};
|
|
39
91
|
|
|
40
92
|
exports.clientVars = (hookName, context) => {
|
|
41
93
|
if (oidcClient == null) return;
|
|
42
94
|
const {permit_displayname_change} = settings;
|
|
43
|
-
return {
|
|
95
|
+
return {ep_openid_connect: {permit_displayname_change}};
|
|
44
96
|
};
|
|
45
97
|
|
|
46
98
|
exports.expressCreateServer = (hookName, {app}) => {
|
|
@@ -50,15 +102,11 @@ exports.expressCreateServer = (hookName, {app}) => {
|
|
|
50
102
|
logger.debug(`Processing ${req.url}`);
|
|
51
103
|
(async () => {
|
|
52
104
|
const params = oidcClient.callbackParams(req);
|
|
53
|
-
const oidcSession = req.session
|
|
105
|
+
const oidcSession = req.session.ep_openid_connect || {};
|
|
54
106
|
const {authParams} = oidcSession;
|
|
55
107
|
if (authParams == null) throw new Error('no authentication paramters found in session state');
|
|
56
108
|
const tokenset = await oidcClient.callback(endpointUrl('callback'), params, authParams);
|
|
57
|
-
|
|
58
|
-
if (settings.prohibited_usernames.indexOf(userinfo.sub) !== -1) {
|
|
59
|
-
throw new Error(`authenticated user's 'sub' claim (${userinfo.sub}) is not permitted`);
|
|
60
|
-
}
|
|
61
|
-
oidcSession.userinfo = userinfo;
|
|
109
|
+
oidcSession.userinfo = await oidcClient.userinfo(tokenset);
|
|
62
110
|
// The user has successfully authenticated, but don't set req.session.user here -- do it in
|
|
63
111
|
// the authenticate hook so that Etherpad can log the authentication success. However, DO "log
|
|
64
112
|
// out" the previous user to force the authenticate hook to run in case the user was already
|
|
@@ -73,10 +121,14 @@ exports.expressCreateServer = (hookName, {app}) => {
|
|
|
73
121
|
});
|
|
74
122
|
app.get(ep('login'), (req, res, next) => {
|
|
75
123
|
logger.debug(`Processing ${req.url}`);
|
|
76
|
-
if (req.session
|
|
77
|
-
const oidcSession = req.session
|
|
124
|
+
if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
|
|
125
|
+
const oidcSession = req.session.ep_openid_connect;
|
|
78
126
|
oidcSession.next = req.query.redirect_uri || settings.base_url;
|
|
79
|
-
oidcSession.authParams = {
|
|
127
|
+
oidcSession.authParams = {
|
|
128
|
+
nonce: generators.nonce(),
|
|
129
|
+
scope: settings.scope.join(' '),
|
|
130
|
+
state: generators.state(),
|
|
131
|
+
};
|
|
80
132
|
res.redirect(oidcClient.authorizationUrl(oidcSession.authParams));
|
|
81
133
|
});
|
|
82
134
|
app.get(ep('logout'), (req, res) => req.session.destroy(() => res.redirect(settings.base_url)));
|
|
@@ -86,20 +138,31 @@ exports.authenticate = (hookName, {req, res, users}) => {
|
|
|
86
138
|
if (oidcClient == null) return;
|
|
87
139
|
logger.debug('authenticate hook for', req.url);
|
|
88
140
|
const {session} = req;
|
|
89
|
-
const {
|
|
141
|
+
const {ep_openid_connect: {userinfo = {}} = {}} = session;
|
|
90
142
|
const {sub} = userinfo;
|
|
91
|
-
if (sub == null
|
|
143
|
+
if (sub == null || // Nullish means the user isn't authenticated.
|
|
144
|
+
typeof sub !== 'string' || // `sub` is used as the username, so it must be a string.
|
|
145
|
+
sub === '' || // Empty string doesn't make sense.
|
|
146
|
+
sub === '__proto__' || // Prevent prototype pollution.
|
|
147
|
+
settings.prohibited_usernames.includes(sub)) {
|
|
92
148
|
// Out of an abundance of caution, clear out the old state, nonce, and userinfo (if present) to
|
|
93
149
|
// force regeneration.
|
|
94
|
-
delete session
|
|
150
|
+
delete session.ep_openid_connect;
|
|
95
151
|
// Authn failed. Let another plugin try to authenticate the user.
|
|
96
152
|
return;
|
|
97
153
|
}
|
|
98
154
|
// Successfully authenticated.
|
|
99
|
-
|
|
155
|
+
logger.info('Successfully authenticated user with userinfo:', userinfo);
|
|
100
156
|
session.user = users[sub];
|
|
101
|
-
session.user.
|
|
102
|
-
|
|
157
|
+
if (session.user == null) session.user = users[sub] = {};
|
|
158
|
+
for (const [propName, descriptor] of Object.entries(settings.user_properties)) {
|
|
159
|
+
if (descriptor.claim != null && descriptor.claim in userinfo) {
|
|
160
|
+
session.user[propName] = userinfo[descriptor.claim];
|
|
161
|
+
} else if ('default' in descriptor && !(propName in session.user)) {
|
|
162
|
+
session.user[propName] = descriptor.default;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
logger.debug('User properties:', session.user);
|
|
103
166
|
return true;
|
|
104
167
|
};
|
|
105
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_openid_connect",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -23,17 +23,18 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/ether/ep_openid_connect#readme",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"openid-client": "^
|
|
26
|
+
"openid-client": "^4.7.4"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"eslint": "^7.
|
|
30
|
-
"eslint-config-etherpad": "^
|
|
29
|
+
"eslint": "^7.32.0",
|
|
30
|
+
"eslint-config-etherpad": "^2.0.2",
|
|
31
|
+
"eslint-plugin-cypress": "^2.12.1",
|
|
31
32
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
32
|
-
"eslint-plugin-mocha": "^
|
|
33
|
+
"eslint-plugin-mocha": "^9.0.0",
|
|
33
34
|
"eslint-plugin-node": "^11.1.0",
|
|
34
35
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
35
|
-
"eslint-plugin-promise": "^
|
|
36
|
-
"eslint-plugin-you-dont-need-lodash-underscore": "^6.
|
|
36
|
+
"eslint-plugin-promise": "^5.1.1",
|
|
37
|
+
"eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0"
|
|
37
38
|
},
|
|
38
39
|
"eslintConfig": {
|
|
39
40
|
"root": true,
|
|
@@ -45,12 +46,12 @@
|
|
|
45
46
|
},
|
|
46
47
|
"funding": {
|
|
47
48
|
"type": "individual",
|
|
48
|
-
"url": "
|
|
49
|
+
"url": "https://etherpad.org/"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"ep_etherpad-lite": ">=1.8.6"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|
|
54
|
-
"node": ">=
|
|
55
|
+
"node": ">=12.13.0"
|
|
55
56
|
}
|
|
56
57
|
}
|