ep_openid_connect 2.3.0 → 2.5.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.
@@ -14,10 +14,6 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
 
16
16
  steps:
17
- - uses: actions/setup-node@v2
18
- with:
19
- node-version: 12
20
-
21
17
  - name: Install libreoffice
22
18
  run: |
23
19
  sudo add-apt-repository -y ppa:libreoffice/ppa
@@ -30,9 +26,6 @@ jobs:
30
26
  with:
31
27
  repository: ether/etherpad-lite
32
28
 
33
- - name: Install all dependencies and symlink for ep_etherpad-lite
34
- run: src/bin/installDeps.sh
35
-
36
29
  - name: Checkout plugin repository
37
30
  uses: actions/checkout@v2
38
31
  with:
@@ -50,6 +43,15 @@ jobs:
50
43
  env:
51
44
  PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
52
45
 
46
+ - uses: actions/setup-node@v2
47
+ with:
48
+ node-version: 12
49
+ cache: 'npm'
50
+ cache-dependency-path: |
51
+ src/package-lock.json
52
+ src/bin/doc/package-lock.json
53
+ node_modules/${{ steps.plugin_name.outputs.plugin_name }}/package-lock.json
54
+
53
55
  - name: Install plugin dependencies
54
56
  run: |
55
57
  cd ./node_modules/"${PLUGIN_NAME}"
@@ -57,5 +59,15 @@ jobs:
57
59
  env:
58
60
  PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
59
61
 
62
+ # This must be run after setting up the plugin, otherwise npm will try to
63
+ # hoist common dependencies by removing them from src/node_modules and
64
+ # installing them in the top-level node_modules. As of v6.14.10, npm's hoist
65
+ # logic appears to be buggy, because it sometimes removes dependencies from
66
+ # src/node_modules but fails to add them to the top-level node_modules. Even
67
+ # if npm correctly hoists the dependencies, the hoisting seems to confuse
68
+ # tools such as `npm outdated`, `npm update`, and some ESLint rules.
69
+ - name: Install Etherpad core dependencies
70
+ run: src/bin/installDeps.sh
71
+
60
72
  - name: Run the backend tests
61
73
  run: cd src && npm test
@@ -15,15 +15,19 @@ jobs:
15
15
  printf %s\\n '::set-output name=name::${{github.event.repository.name}} ${{ github.workflow }} - ${{ github.job }}'
16
16
  printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
17
17
 
18
- - uses: actions/setup-node@v2
19
- with:
20
- node-version: 12
21
-
22
18
  - name: Check out Etherpad core
23
19
  uses: actions/checkout@v2
24
20
  with:
25
21
  repository: ether/etherpad-lite
26
22
 
23
+ - uses: actions/setup-node@v2
24
+ with:
25
+ node-version: 12
26
+ cache: 'npm'
27
+ cache-dependency-path: |
28
+ src/package-lock.json
29
+ src/bin/doc/package-lock.json
30
+
27
31
  - name: Check out the plugin
28
32
  uses: actions/checkout@v2
29
33
  with:
@@ -32,6 +32,11 @@ jobs:
32
32
  - uses: actions/setup-node@v1
33
33
  with:
34
34
  node-version: 12
35
+ cache: 'npm'
36
+ cache-dependency-path: |
37
+ ../etherpad-lite/src/package-lock.json
38
+ ../etherpad-lite/src/bin/doc/package-lock.json
39
+ package-lock.json
35
40
  # All of ep_etherpad-lite's devDependencies are installed because the
36
41
  # plugin might do `require('ep_etherpad-lite/node_modules/${devDep}')`.
37
42
  # Eventually it would be nice to create an ESLint plugin that prohibits
@@ -62,6 +67,7 @@ jobs:
62
67
  with:
63
68
  node-version: 12
64
69
  registry-url: https://registry.npmjs.org/
70
+ cache: 'npm'
65
71
  - name: Bump version (patch)
66
72
  run: |
67
73
  LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1
@@ -85,3 +91,7 @@ jobs:
85
91
  - run: npm publish
86
92
  env:
87
93
  NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
94
+ - name: Add package to etherpad organization
95
+ run: npm access grant read-write etherpad:developers
96
+ env:
97
+ NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
package/index.js CHANGED
@@ -135,7 +135,7 @@ exports.expressCreateServer = (hookName, {app}) => {
135
135
  // userinfo should not be stored in req.session until after all checks have passed. (Otherwise
136
136
  // it would be too easy to accidentally introduce a vulnerability.)
137
137
  oidcSession.userinfo = userinfo;
138
- res.redirect(oidcSession.next || settings.base_url);
138
+ res.redirect(303, oidcSession.next || settings.base_url);
139
139
  // Defer deletion of state until success so that the user can reload the page to retry after a
140
140
  // transient backchannel failure.
141
141
  delete oidcSession.callbackChecks;
@@ -152,7 +152,6 @@ exports.expressCreateServer = (hookName, {app}) => {
152
152
  }
153
153
  if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
154
154
  const oidcSession = req.session.ep_openid_connect;
155
- oidcSession.next = req.query.redirect_uri || settings.base_url;
156
155
  const commonParams = {
157
156
  nonce: generators.nonce(),
158
157
  scope: settings.scope.join(' '),
@@ -162,7 +161,7 @@ exports.expressCreateServer = (hookName, {app}) => {
162
161
  ...commonParams,
163
162
  code_verifier: generators.codeVerifier(), // RFC7636
164
163
  };
165
- res.redirect(oidcClient.authorizationUrl({
164
+ res.redirect(303, oidcClient.authorizationUrl({
166
165
  ...commonParams,
167
166
  // RFC7636
168
167
  code_challenge: generators.codeChallenge(oidcSession.callbackChecks.code_verifier),
@@ -175,7 +174,7 @@ exports.expressCreateServer = (hookName, {app}) => {
175
174
  logger.warn('Not configured; ignoring request.');
176
175
  return next();
177
176
  }
178
- req.session.destroy(() => res.redirect(settings.base_url));
177
+ req.session.destroy(() => res.redirect(303, settings.base_url));
179
178
  });
180
179
  };
181
180
 
@@ -207,8 +206,41 @@ exports.authenticate = (hookName, {req, res, users}) => {
207
206
 
208
207
  exports.authnFailure = (hookName, {req, res}) => {
209
208
  if (oidcClient == null) return;
210
- const url = new URL(req.url.substr(1), settings.base_url).toString();
211
- res.redirect(`${endpointUrl('login')}?redirect_uri=${encodeURIComponent(url)}`);
209
+ // Normally the user is redirected to the login page which would then redirect the user back once
210
+ // authenticated. For non-GET requests, send a 401 instead because users can't be redirected back.
211
+ // Also send a 401 if an Authorization header is present to facilitate API error handling.
212
+ //
213
+ // 401 is the status that most closely matches the desired semantics. However, RFC7235 section
214
+ // 3.1 says, "The server generating a 401 response MUST send a WWW-Authenticate header field
215
+ // containing at least one challenge applicable to the target resource." Etherpad uses a token
216
+ // (signed session identifier) transmitted via cookie for authentication, but there is no
217
+ // standard authentication scheme name for that. So we use a non-standard name here.
218
+ //
219
+ // We could theoretically implement Bearer authorization (RFC6750), but it's unclear to me how
220
+ // to do this correctly and securely:
221
+ // * The userinfo endpoint is meant for the OAuth client, not the resource server, so it
222
+ // shouldn't be used to look up claims.
223
+ // * In general, access tokens might be opaque (not JWTs) so we can't get claims by parsing
224
+ // them.
225
+ // * The token introspection endpoint should return scope and subject (I think?), but probably
226
+ // not claims.
227
+ // * If claims can't be used to convey access level, how is it conveyed? Scope? Resource
228
+ // indicators (RFC8707)?
229
+ // * How is intended audience checked? Or is introspection guaranteed to do that for us?
230
+ // * Should tokens be limited to a particular pad?
231
+ // * Bearer tokens are only meant to convey authorization; authentication is handled by the
232
+ // authorization server. Should Bearer tokens be processed during the authorize hook?
233
+ // * How should bearer authentication interact with authorization plugins?
234
+ // * How should bearer authentication interact with plugins that add new endpoints?
235
+ // * Would we have to implement our own OAuth server to issue access tokens?
236
+ res.header('WWW-Authenticate', 'Etherpad');
237
+ if (!['GET', 'HEAD'].includes(req.method) || req.headers.authorization) {
238
+ res.status(401).end();
239
+ return true;
240
+ }
241
+ if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
242
+ req.session.ep_openid_connect.next = new URL(req.url.slice(1), settings.base_url).toString();
243
+ res.redirect(303, endpointUrl('login'));
212
244
  return true;
213
245
  };
214
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "2.3.0",
3
+ "version": "2.5.2",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -54,6 +54,6 @@
54
54
  "ep_etherpad-lite": ">=1.8.6"
55
55
  },
56
56
  "engines": {
57
- "node": ">=12.13.0"
57
+ "node": ">=12.17.0"
58
58
  }
59
59
  }
@@ -6,7 +6,7 @@ const common = require('ep_etherpad-lite/tests/backend/common');
6
6
  const logger = common.logger;
7
7
 
8
8
  const followRedirects = async (agent, res) => {
9
- if (![302, 303].includes(res.status)) return res;
9
+ if (res.status !== 303) return res;
10
10
  const url = new URL(res.headers.location, res.request.url);
11
11
  logger.debug(`redirected to ${url}`);
12
12
  return await followRedirects(agent, await agent.get(url));
@@ -63,19 +63,14 @@ describe(__filename, function () {
63
63
  });
64
64
 
65
65
  it('not logged in redirects to login endpoint', async function () {
66
- const params = new URLSearchParams();
67
- params.append('redirect_uri', new URL('/', common.baseUrl));
68
- const wantRedirect = new URL(`/ep_openid_connect/login?${params}`, common.baseUrl).toString();
69
66
  await agent.get(common.baseUrl)
70
- .expect(302)
71
- .expect('location', wantRedirect);
67
+ .expect(303)
68
+ .expect('location', new URL('/ep_openid_connect/login', common.baseUrl).toString());
72
69
  });
73
70
 
74
71
  it('login endpoint redirects to authorization URL', async function () {
75
- const params = new URLSearchParams();
76
- params.append('redirect_uri', new URL('/', common.baseUrl));
77
- await agent.get(new URL(`/ep_openid_connect/login?${params}`, common.baseUrl))
78
- .expect(302)
72
+ await agent.get(new URL('/ep_openid_connect/login', common.baseUrl))
73
+ .expect(303)
79
74
  .expect('location', /.*/)
80
75
  .expect((res) => {
81
76
  assert(res.headers.location.startsWith(`${new URL('/auth', issuer)}?`));
@@ -210,13 +205,24 @@ describe(__filename, function () {
210
205
  assert.equal(res.request.url, url);
211
206
  assert.equal(res.status, 200);
212
207
  await agent.get(new URL('/ep_openid_connect/logout', common.baseUrl))
213
- .expect(302)
208
+ .expect(303)
214
209
  .expect('location', new URL('/', common.baseUrl).toString());
215
210
  // Visiting any page that requires authentication should now redirect to the login page.
216
211
  await agent.get(new URL('/', common.baseUrl))
217
- .expect(302)
218
- .expect('location', /./)
219
- .expect((res) => assert(res.headers.location.startsWith(
220
- new URL('/ep_openid_connect/login?', common.baseUrl).toString())));
212
+ .expect(303)
213
+ .expect('location', new URL('/ep_openid_connect/login', common.baseUrl).toString());
214
+ });
215
+
216
+ it('HTTP PUT gives 401 instead of redirect', async function () {
217
+ await agent.put(common.baseUrl)
218
+ .expect(401)
219
+ .expect('www-authenticate', 'Etherpad');
220
+ });
221
+
222
+ it('Authorization header results in 401 instead of redirect', async function () {
223
+ await agent.get(common.baseUrl)
224
+ .set('Authorization', 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
225
+ .expect(401)
226
+ .expect('www-authenticate', 'Etherpad');
221
227
  });
222
228
  });