ep_openid_connect 2.0.8 → 2.2.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/.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 +63 -7
- package/index.js +74 -35
- package/package.json +7 -7
|
@@ -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
|
@@ -52,10 +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
|
|
56
|
-
|
|
55
|
+
* `displayname_claim` (optional; defaults to `"name"`): The default value of
|
|
56
|
+
`user_properties.displayname.claim`. **Deprecated:** Set
|
|
57
|
+
`user_properties.displayname.claim` instead.
|
|
57
58
|
* `response_types` (optional; defaults to `["code"]`): List of OpenID Connect
|
|
58
59
|
response types.
|
|
60
|
+
* `scope` (optional; defaults to `["openid"]`): List of OAuth2 scope strings.
|
|
59
61
|
* `permit_displayname_change` (optional; defaults to `false`): Whether users may
|
|
60
62
|
change their displayed name.
|
|
61
63
|
* `prohibited_usernames` (optional; defaults to `['admin', 'guest']`): List of
|
|
@@ -64,6 +66,9 @@ Etherpad's `requireAuthentication` setting must be `true`.
|
|
|
64
66
|
the users in the `users` setting and to avoid conflicts with other plugins
|
|
65
67
|
(such as
|
|
66
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.
|
|
67
72
|
|
|
68
73
|
## Interaction with the `users` Setting
|
|
69
74
|
|
|
@@ -73,15 +78,18 @@ identifier for the user.) Many identity providers (such as GitLab) identify
|
|
|
73
78
|
users by a numeric user ID, so the `sub` claim (and thus the Etherpad username)
|
|
74
79
|
will probably look something like "5374".
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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":
|
|
81
88
|
|
|
82
89
|
```json
|
|
83
90
|
"users": {
|
|
84
91
|
"5374": {
|
|
92
|
+
"displayname": "Firstname Lastname",
|
|
85
93
|
"readOnly": true
|
|
86
94
|
}
|
|
87
95
|
},
|
|
@@ -92,6 +100,54 @@ you can use the `prohibited_usernames` settings to force an authentication error
|
|
|
92
100
|
if the `sub` claim happens to match. This is useful for preventing a malicious
|
|
93
101
|
identity provider from gaining admin access to your Etherpad instance.
|
|
94
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
|
+
|
|
95
151
|
## Interaction with the ep\_readonly\_guest Plugin
|
|
96
152
|
|
|
97
153
|
The [ep\_readonly\_guest
|
package/index.js
CHANGED
|
@@ -5,19 +5,29 @@ 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
|
|
|
22
|
+
const validateSubClaim = (sub) => {
|
|
23
|
+
if (typeof sub !== 'string' || // 'sub' claim must exist as a string per OIDC spec.
|
|
24
|
+
sub === '' || // Empty string doesn't make sense.
|
|
25
|
+
sub === '__proto__' || // Prevent prototype pollution.
|
|
26
|
+
settings.prohibited_usernames.includes(sub)) {
|
|
27
|
+
throw new Error('invalid sub claim');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
const discoverIssuer = async (issuerUrl) => {
|
|
22
32
|
issuerUrl = new URL(issuerUrl);
|
|
23
33
|
// https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery says that the URI
|
|
@@ -54,24 +64,31 @@ const getIssuer = async (settings) => {
|
|
|
54
64
|
return new Issuer(settings.issuer_metadata);
|
|
55
65
|
};
|
|
56
66
|
|
|
57
|
-
exports.loadSettings = async (hookName, {settings: {
|
|
67
|
+
exports.loadSettings = async (hookName, {settings: {ep_openid_connect: config = {}}}) => {
|
|
58
68
|
Object.assign(settings, config);
|
|
59
69
|
for (const setting of ['base_url', 'client_id', 'client_secret']) {
|
|
60
70
|
if (!settings[setting]) {
|
|
61
|
-
logger.error(`Required setting missing from settings.json:
|
|
71
|
+
logger.error(`Required setting missing from settings.json: ep_openid_connect.${setting}`);
|
|
62
72
|
return;
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
if (!settings.issuer && !settings.issuer_metadata) {
|
|
66
|
-
logger.error(
|
|
76
|
+
logger.error(
|
|
77
|
+
'Either ep_openid_connect.issuer or ep_openid_connect.issuer_metadata must be set');
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
if (settings.issuer && settings.issuer_metadata) {
|
|
70
|
-
logger.warn(
|
|
71
|
-
|
|
81
|
+
logger.warn('Ignoring ep_openid_connect.issuer_metadata setting ' +
|
|
82
|
+
'because ep_openid_connect.issuer is set');
|
|
72
83
|
}
|
|
73
84
|
// Make sure base_url ends with '/' so that relative URLs are appended:
|
|
74
85
|
if (!settings.base_url.endsWith('/')) settings.base_url += '/';
|
|
86
|
+
settings.user_properties = {
|
|
87
|
+
displayname: {claim: settings.displayname_claim},
|
|
88
|
+
...settings.user_properties,
|
|
89
|
+
// The username property must always match the key used in settings.users.
|
|
90
|
+
username: {claim: 'sub'},
|
|
91
|
+
};
|
|
75
92
|
oidcClient = new (await getIssuer(settings)).Client({
|
|
76
93
|
client_id: settings.client_id,
|
|
77
94
|
client_secret: settings.client_secret,
|
|
@@ -84,44 +101,61 @@ exports.loadSettings = async (hookName, {settings: {[pluginName]: config = {}}})
|
|
|
84
101
|
exports.clientVars = (hookName, context) => {
|
|
85
102
|
if (oidcClient == null) return;
|
|
86
103
|
const {permit_displayname_change} = settings;
|
|
87
|
-
return {
|
|
104
|
+
return {ep_openid_connect: {permit_displayname_change}};
|
|
88
105
|
};
|
|
89
106
|
|
|
90
107
|
exports.expressCreateServer = (hookName, {app}) => {
|
|
91
108
|
if (oidcClient == null) return;
|
|
92
109
|
logger.debug('Configuring auth routes');
|
|
93
|
-
app.get(ep('callback'), (req, res, next) => {
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
app.get(ep('callback'), async (req, res, next) => {
|
|
111
|
+
// This handler MUST NOT redirect to a page that requires authentication if there is a problem,
|
|
112
|
+
// otherwise the user could be caught in an infinite redirect loop.
|
|
113
|
+
try {
|
|
114
|
+
logger.debug(`Processing ${req.url}`);
|
|
96
115
|
const params = oidcClient.callbackParams(req);
|
|
97
|
-
const oidcSession = req.session
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
const oidcSession = req.session.ep_openid_connect || {};
|
|
117
|
+
if (oidcSession.callbackChecks == null) throw new Error('missing authentication checks');
|
|
118
|
+
const tokenset =
|
|
119
|
+
await oidcClient.callback(endpointUrl('callback'), params, oidcSession.callbackChecks);
|
|
101
120
|
const userinfo = await oidcClient.userinfo(tokenset);
|
|
102
|
-
|
|
103
|
-
throw new Error(`authenticated user's 'sub' claim (${userinfo.sub}) is not permitted`);
|
|
104
|
-
}
|
|
105
|
-
oidcSession.userinfo = userinfo;
|
|
121
|
+
validateSubClaim(userinfo.sub);
|
|
106
122
|
// The user has successfully authenticated, but don't set req.session.user here -- do it in
|
|
107
123
|
// the authenticate hook so that Etherpad can log the authentication success. However, DO "log
|
|
108
124
|
// out" the previous user to force the authenticate hook to run in case the user was already
|
|
109
125
|
// authenticated as someone else.
|
|
110
126
|
delete req.session.user;
|
|
127
|
+
// userinfo should not be stored in req.session until after all checks have passed. (Otherwise
|
|
128
|
+
// it would be too easy to accidentally introduce a vulnerability.)
|
|
129
|
+
oidcSession.userinfo = userinfo;
|
|
111
130
|
res.redirect(oidcSession.next || settings.base_url);
|
|
112
131
|
// Defer deletion of state until success so that the user can reload the page to retry after a
|
|
113
132
|
// transient backchannel failure.
|
|
114
|
-
delete oidcSession.
|
|
133
|
+
delete oidcSession.callbackChecks;
|
|
115
134
|
delete oidcSession.next;
|
|
116
|
-
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return next(err);
|
|
137
|
+
}
|
|
117
138
|
});
|
|
118
139
|
app.get(ep('login'), (req, res, next) => {
|
|
119
140
|
logger.debug(`Processing ${req.url}`);
|
|
120
|
-
if (req.session
|
|
121
|
-
const oidcSession = req.session
|
|
141
|
+
if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
|
|
142
|
+
const oidcSession = req.session.ep_openid_connect;
|
|
122
143
|
oidcSession.next = req.query.redirect_uri || settings.base_url;
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
const commonParams = {
|
|
145
|
+
nonce: generators.nonce(),
|
|
146
|
+
scope: settings.scope.join(' '),
|
|
147
|
+
state: generators.state(),
|
|
148
|
+
};
|
|
149
|
+
oidcSession.callbackChecks = {
|
|
150
|
+
...commonParams,
|
|
151
|
+
code_verifier: generators.codeVerifier(), // RFC7636
|
|
152
|
+
};
|
|
153
|
+
res.redirect(oidcClient.authorizationUrl({
|
|
154
|
+
...commonParams,
|
|
155
|
+
// RFC7636
|
|
156
|
+
code_challenge: generators.codeChallenge(oidcSession.callbackChecks.code_verifier),
|
|
157
|
+
code_challenge_method: 'S256',
|
|
158
|
+
}));
|
|
125
159
|
});
|
|
126
160
|
app.get(ep('logout'), (req, res) => req.session.destroy(() => res.redirect(settings.base_url)));
|
|
127
161
|
};
|
|
@@ -129,21 +163,26 @@ exports.expressCreateServer = (hookName, {app}) => {
|
|
|
129
163
|
exports.authenticate = (hookName, {req, res, users}) => {
|
|
130
164
|
if (oidcClient == null) return;
|
|
131
165
|
logger.debug('authenticate hook for', req.url);
|
|
132
|
-
const {
|
|
133
|
-
|
|
134
|
-
const {sub} = userinfo;
|
|
135
|
-
if (sub == null) {
|
|
166
|
+
const {ep_openid_connect: {userinfo} = {}} = req.session;
|
|
167
|
+
if (userinfo == null) { // Nullish means the user isn't authenticated.
|
|
136
168
|
// Out of an abundance of caution, clear out the old state, nonce, and userinfo (if present) to
|
|
137
169
|
// force regeneration.
|
|
138
|
-
delete session
|
|
170
|
+
delete req.session.ep_openid_connect;
|
|
139
171
|
// Authn failed. Let another plugin try to authenticate the user.
|
|
140
172
|
return;
|
|
141
173
|
}
|
|
142
174
|
// Successfully authenticated.
|
|
143
|
-
|
|
144
|
-
session.user = users[sub];
|
|
145
|
-
session.user.
|
|
146
|
-
|
|
175
|
+
logger.info('Successfully authenticated user with userinfo:', userinfo);
|
|
176
|
+
req.session.user = users[userinfo.sub];
|
|
177
|
+
if (req.session.user == null) req.session.user = users[userinfo.sub] = {};
|
|
178
|
+
for (const [propName, descriptor] of Object.entries(settings.user_properties)) {
|
|
179
|
+
if (descriptor.claim != null && descriptor.claim in userinfo) {
|
|
180
|
+
req.session.user[propName] = userinfo[descriptor.claim];
|
|
181
|
+
} else if ('default' in descriptor && !(propName in req.session.user)) {
|
|
182
|
+
req.session.user[propName] = descriptor.default;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
logger.debug('User properties:', req.session.user);
|
|
147
186
|
return true;
|
|
148
187
|
};
|
|
149
188
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_openid_connect",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"openid-client": "^4.7.4"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"eslint": "^7.
|
|
30
|
-
"eslint-config-etherpad": "^2.0.
|
|
31
|
-
"eslint-plugin-cypress": "^2.
|
|
29
|
+
"eslint": "^7.32.0",
|
|
30
|
+
"eslint-config-etherpad": "^2.0.2",
|
|
31
|
+
"eslint-plugin-cypress": "^2.12.1",
|
|
32
32
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
33
33
|
"eslint-plugin-mocha": "^9.0.0",
|
|
34
34
|
"eslint-plugin-node": "^11.1.0",
|
|
35
35
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
36
|
-
"eslint-plugin-promise": "^5.1.
|
|
36
|
+
"eslint-plugin-promise": "^5.1.1",
|
|
37
37
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0"
|
|
38
38
|
},
|
|
39
39
|
"eslintConfig": {
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"funding": {
|
|
48
48
|
"type": "individual",
|
|
49
|
-
"url": "
|
|
49
|
+
"url": "https://etherpad.org/"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ep_etherpad-lite": ">=1.8.6"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
55
|
+
"node": ">=12.13.0"
|
|
56
56
|
}
|
|
57
57
|
}
|