ep_openid_connect 2.0.5 → 2.0.9

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.
@@ -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/${{github.event.repository.name}}
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/${{github.event.repository.name}}
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
- - run: git config user.name 'github-actions[bot]'
64
- - run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
65
- - run: npm ci
66
- - run: npm version patch
67
- - run: git push --follow-tags
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,8 +34,18 @@ Etherpad's `requireAuthentication` setting must be `true`.
34
34
 
35
35
  ### Configuration Details
36
36
 
37
- * `issuer` (required): The base URL of the OpenID Connect identity provider,
38
- used to perform discovery of the endpoints.
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.
package/index.js CHANGED
@@ -7,41 +7,88 @@ const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
7
7
 
8
8
  const pluginName = 'ep_openid_connect';
9
9
  const logger = log4js.getLogger(pluginName);
10
- const settings = {};
10
+ const settings = {
11
+ displayname_claim: 'name',
12
+ response_types: ['code'],
13
+ permit_displayname_change: false,
14
+ prohibited_usernames: ['admin', 'guest'],
15
+ };
11
16
  let oidcClient = null;
12
17
 
13
18
  const ep = (endpoint) => `/${pluginName}/${endpoint}`;
14
19
  const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
15
20
 
16
- exports.loadSettings = async (hookName, {settings: globalSettings}) => {
17
- const mySettings = globalSettings[pluginName];
21
+ const discoverIssuer = async (issuerUrl) => {
22
+ issuerUrl = new URL(issuerUrl);
23
+ // https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery says that the URI
24
+ // must not have query or fragment components.
25
+ if (issuerUrl.search) {
26
+ throw new Error(`Unexpected query in issuer URL (${issuerUrl}): ${issuerUrl.search}`);
27
+ }
28
+ if (issuerUrl.hash) {
29
+ throw new Error(`Unexpected fragment in issuer URL (${issuerUrl}): ${issuerUrl.hash}`);
30
+ }
31
+ let issuer;
32
+ try {
33
+ issuer = await Issuer.discover(issuerUrl.href);
34
+ } catch (err) {
35
+ // The URL used to get the issuer metadata doesn't exactly follow RFC 8615; see:
36
+ // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
37
+ const discoveryUrl = new URL(issuerUrl);
38
+ if (!discoveryUrl.pathname.includes('/.well-known/')) {
39
+ discoveryUrl.pathname =
40
+ `${discoveryUrl.pathname.replace(/\/$/, '')}/.well-known/openid-configuration`;
41
+ }
42
+ logger.error(
43
+ 'Failed to discover issuer metadata via OpenID Connect Discovery ' +
44
+ '(https://openid.net/specs/openid-connect-discovery-1_0.html). ' +
45
+ `Does your issuer support Discovery? (hint: ${discoveryUrl})`);
46
+ throw err;
47
+ }
48
+ logger.info('OpenID Connect Discovery complete.');
49
+ return issuer;
50
+ };
18
51
 
19
- if (!mySettings) logger.error(`Expecting an ${pluginName} block in settings.`);
20
- for (const setting of ['base_url', 'client_id', 'client_secret', 'issuer']) {
21
- if (!mySettings[setting]) logger.error(`Expecting an ${pluginName}.${setting} setting.`);
52
+ const getIssuer = async (settings) => {
53
+ if (settings.issuer) return await discoverIssuer(settings.issuer);
54
+ return new Issuer(settings.issuer_metadata);
55
+ };
56
+
57
+ exports.loadSettings = async (hookName, {settings: {[pluginName]: config = {}}}) => {
58
+ Object.assign(settings, config);
59
+ for (const setting of ['base_url', 'client_id', 'client_secret']) {
60
+ if (!settings[setting]) {
61
+ logger.error(`Required setting missing from settings.json: ${pluginName}.${setting}`);
62
+ return;
63
+ }
64
+ }
65
+ if (!settings.issuer && !settings.issuer_metadata) {
66
+ logger.error(`Either ${pluginName}.issuer or ${pluginName}.issuer_metadata must be set`);
67
+ return;
68
+ }
69
+ if (settings.issuer && settings.issuer_metadata) {
70
+ logger.warn(
71
+ `Ignoring ${pluginName}.issuer_metadata setting because ${pluginName}.issuer is set`);
22
72
  }
23
- Object.assign(settings, mySettings);
24
73
  // Make sure base_url ends with '/' so that relative URLs are appended:
25
74
  if (!settings.base_url.endsWith('/')) settings.base_url += '/';
26
- settings.displayname_claim = settings.displayname_claim || 'name';
27
- settings.response_types = settings.response_types || ['code'];
28
- settings.permit_displayname_change = settings.permit_displayname_change || false;
29
- settings.prohibited_usernames = settings.prohibited_usernames || ['admin', 'guest'];
30
- oidcClient = new (await Issuer.discover(settings.issuer)).Client({
75
+ oidcClient = new (await getIssuer(settings)).Client({
31
76
  client_id: settings.client_id,
32
77
  client_secret: settings.client_secret,
33
78
  response_types: settings.response_types,
34
79
  redirect_uris: [endpointUrl('callback')],
35
80
  });
36
- logger.info('Client discovery complete. Configured.');
81
+ logger.info('Configured.');
37
82
  };
38
83
 
39
- exports.clientVars = (hookName, context, callback) => {
84
+ exports.clientVars = (hookName, context) => {
85
+ if (oidcClient == null) return;
40
86
  const {permit_displayname_change} = settings;
41
- return callback({[pluginName]: {permit_displayname_change}});
87
+ return {[pluginName]: {permit_displayname_change}};
42
88
  };
43
89
 
44
90
  exports.expressCreateServer = (hookName, {app}) => {
91
+ if (oidcClient == null) return;
45
92
  logger.debug('Configuring auth routes');
46
93
  app.get(ep('callback'), (req, res, next) => {
47
94
  logger.debug(`Processing ${req.url}`);
@@ -79,7 +126,8 @@ exports.expressCreateServer = (hookName, {app}) => {
79
126
  app.get(ep('logout'), (req, res) => req.session.destroy(() => res.redirect(settings.base_url)));
80
127
  };
81
128
 
82
- exports.authenticate = (hookName, {req, res, users}, cb) => {
129
+ exports.authenticate = (hookName, {req, res, users}) => {
130
+ if (oidcClient == null) return;
83
131
  logger.debug('authenticate hook for', req.url);
84
132
  const {session} = req;
85
133
  const {[pluginName]: {userinfo = {}} = {}} = session;
@@ -89,29 +137,31 @@ exports.authenticate = (hookName, {req, res, users}, cb) => {
89
137
  // force regeneration.
90
138
  delete session[pluginName];
91
139
  // Authn failed. Let another plugin try to authenticate the user.
92
- return cb([]);
140
+ return;
93
141
  }
94
142
  // Successfully authenticated.
95
143
  if (users[sub] == null) users[sub] = {};
96
144
  session.user = users[sub];
97
145
  session.user.username = sub;
98
146
  session.user.displayname = userinfo[settings.displayname_claim] || session.user.displayname;
99
- return cb([true]);
147
+ return true;
100
148
  };
101
149
 
102
- exports.authnFailure = (hookName, {req, res}, cb) => {
150
+ exports.authnFailure = (hookName, {req, res}) => {
151
+ if (oidcClient == null) return;
103
152
  const url = new URL(req.url.substr(1), settings.base_url).toString();
104
153
  res.redirect(`${endpointUrl('login')}?redirect_uri=${encodeURIComponent(url)}`);
105
- return cb([true]);
154
+ return true;
106
155
  };
107
156
 
108
- exports.handleMessage = async (hookName, {message, client}) => {
157
+ exports.handleMessage = async (hookName, {message, socket}) => {
158
+ if (oidcClient == null) return;
109
159
  logger.debug('handleMessage hook', message);
110
- const {user: {displayname} = {}} = client.client.request.session;
160
+ const {user: {displayname} = {}} = socket.client.request.session;
111
161
  if (!displayname) return;
112
162
  if (message.type === 'CLIENT_READY') {
113
163
  logger.debug(
114
- `CLIENT_READY ${client.id}: Setting username for token ${message.token} to ${displayname}`
164
+ `CLIENT_READY ${socket.id}: Setting username for token ${message.token} to ${displayname}`
115
165
  );
116
166
  // TODO: author ID might come from session ID, not token.
117
167
  const authorId = await authorManager.getAuthor4Token(message.token);
@@ -123,7 +173,8 @@ exports.handleMessage = async (hookName, {message, client}) => {
123
173
  }
124
174
  };
125
175
 
126
- exports.preAuthorize = (hookName, {req}, cb) => {
127
- if (req.path.startsWith(ep(''))) return cb([true]);
128
- return cb([]);
176
+ exports.preAuthorize = (hookName, {req}) => {
177
+ if (oidcClient == null) return;
178
+ if (req.path.startsWith(ep(''))) return true;
179
+ return;
129
180
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "2.0.5",
3
+ "version": "2.0.9",
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": "^3.15.9"
26
+ "openid-client": "^4.7.4"
27
27
  },
28
28
  "devDependencies": {
29
- "eslint": "^7.18.0",
30
- "eslint-config-etherpad": "^1.0.24",
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": "^8.0.0",
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": "^4.2.1",
36
- "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0"
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": "http://etherpad.org/"
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": ">=10.13.0"
55
+ "node": ">=12.13.0"
55
56
  }
56
57
  }
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  exports.postToolbarInit = () => {
4
- if (!clientVars.ep_openid_connect.permit_displayname_change) {
5
- $('#myusernameedit').attr('disabled', true);
6
- }
4
+ const {ep_openid_connect: {permit_displayname_change = true} = {}} = clientVars;
5
+ if (!permit_displayname_change) $('#myusernameedit').attr('disabled', true);
7
6
  };