ep_openid_connect 2.4.0 → 2.5.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/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;
@@ -161,7 +161,7 @@ exports.expressCreateServer = (hookName, {app}) => {
161
161
  ...commonParams,
162
162
  code_verifier: generators.codeVerifier(), // RFC7636
163
163
  };
164
- res.redirect(oidcClient.authorizationUrl({
164
+ res.redirect(303, oidcClient.authorizationUrl({
165
165
  ...commonParams,
166
166
  // RFC7636
167
167
  code_challenge: generators.codeChallenge(oidcSession.callbackChecks.code_verifier),
@@ -174,7 +174,7 @@ exports.expressCreateServer = (hookName, {app}) => {
174
174
  logger.warn('Not configured; ignoring request.');
175
175
  return next();
176
176
  }
177
- req.session.destroy(() => res.redirect(settings.base_url));
177
+ req.session.destroy(() => res.redirect(303, settings.base_url));
178
178
  });
179
179
  };
180
180
 
@@ -206,9 +206,41 @@ exports.authenticate = (hookName, {req, res, users}) => {
206
206
 
207
207
  exports.authnFailure = (hookName, {req, res}) => {
208
208
  if (oidcClient == null) return;
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
+ }
209
241
  if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
210
242
  req.session.ep_openid_connect.next = new URL(req.url.slice(1), settings.base_url).toString();
211
- res.redirect(endpointUrl('login'));
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.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -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));
@@ -64,13 +64,13 @@ describe(__filename, function () {
64
64
 
65
65
  it('not logged in redirects to login endpoint', async function () {
66
66
  await agent.get(common.baseUrl)
67
- .expect(302)
67
+ .expect(303)
68
68
  .expect('location', new URL('/ep_openid_connect/login', common.baseUrl).toString());
69
69
  });
70
70
 
71
71
  it('login endpoint redirects to authorization URL', async function () {
72
72
  await agent.get(new URL('/ep_openid_connect/login', common.baseUrl))
73
- .expect(302)
73
+ .expect(303)
74
74
  .expect('location', /.*/)
75
75
  .expect((res) => {
76
76
  assert(res.headers.location.startsWith(`${new URL('/auth', issuer)}?`));
@@ -205,13 +205,24 @@ describe(__filename, function () {
205
205
  assert.equal(res.request.url, url);
206
206
  assert.equal(res.status, 200);
207
207
  await agent.get(new URL('/ep_openid_connect/logout', common.baseUrl))
208
- .expect(302)
208
+ .expect(303)
209
209
  .expect('location', new URL('/', common.baseUrl).toString());
210
210
  // Visiting any page that requires authentication should now redirect to the login page.
211
211
  await agent.get(new URL('/', common.baseUrl))
212
- .expect(302)
213
- .expect('location', /./)
214
- .expect((res) => assert(res.headers.location.startsWith(
215
- 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');
216
227
  });
217
228
  });