ep_openid_connect 3.0.24 → 3.0.26

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/README.md CHANGED
@@ -49,6 +49,11 @@ Etherpad's `requireAuthentication` setting must be `true`.
49
49
  * `client_id` (required): The OAuth2 client ID issued by the identity provider.
50
50
  * `client_secret` (required): The OAuth2 client secret issued by the identity
51
51
  provider.
52
+ * `token_endpoint_auth_method` (optional; defaults to `client_secret_basic`):
53
+ Client authentication method to use for token endpoint requests. Supported
54
+ values are `client_secret_basic` and `client_secret_post`. Use
55
+ `client_secret_post` if your identity provider expects the client secret in
56
+ the token request body instead of the HTTP `Authorization` header.
52
57
  * `base_url` (required): The public base Etherpad URL. When registering Etherpad
53
58
  with your identity provider, the redirect URL (a.k.a. callback URL) is this
54
59
  base URL plus `/ep_openid_connect/callback`.
package/index.js CHANGED
@@ -10,6 +10,7 @@ for (const level of ['debug', 'info', 'warn', 'error']) {
10
10
  const defaultSettings = {
11
11
  prohibited_usernames: ['admin', 'guest'],
12
12
  scope: ['openid'],
13
+ token_endpoint_auth_method: 'client_secret_basic',
13
14
  user_properties: {},
14
15
  };
15
16
  let settings;
@@ -33,6 +34,7 @@ const validSettings = new Ajv().compile({
33
34
  issuer_metadata: {},
34
35
  prohibited_usernames: {elements: {type: 'string'}},
35
36
  scope: {elements: {type: 'string'}},
37
+ token_endpoint_auth_method: {enum: ['client_secret_basic', 'client_secret_post']},
36
38
  user_properties: {values: {
37
39
  optionalProperties: {
38
40
  claim: {type: 'string'},
@@ -50,6 +52,15 @@ const validSettings = new Ajv().compile({
50
52
  const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
51
53
  const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
52
54
 
55
+ const callbackUrlFromRequest = (req) => {
56
+ const callbackUrl = new URL(endpointUrl('callback'));
57
+ const requestUrl = new URL(req.originalUrl || req.url, settings.base_url);
58
+ for (const [key, value] of requestUrl.searchParams) {
59
+ callbackUrl.searchParams.append(key, value);
60
+ }
61
+ return callbackUrl;
62
+ };
63
+
53
64
  const validateSubClaim = (sub) => {
54
65
  if (typeof sub !== 'string' || // 'sub' claim must exist as a string per OIDC spec.
55
66
  sub === '' || // Empty string doesn't make sense.
@@ -80,8 +91,8 @@ const discoverConfig = async (issuerUrl, clientId, clientAuth) => {
80
91
  // openid-client@6 rejects http:// issuers by default; opt back in for
81
92
  // localhost / private-network providers (matches v5 behaviour).
82
93
  const options = url.protocol === 'http:'
83
- ? {execute: [oidc.allowInsecureRequests]}
84
- : undefined;
94
+ ? {execute: [oidc.allowInsecureRequests]}
95
+ : undefined;
85
96
  try {
86
97
  return await oidc.discovery(url, clientId, undefined, clientAuth, options);
87
98
  } catch (err) {
@@ -100,11 +111,25 @@ const discoverConfig = async (issuerUrl, clientId, clientAuth) => {
100
111
  }
101
112
  };
102
113
 
114
+ const getClientAuth = (settings) => {
115
+ switch (settings.token_endpoint_auth_method) {
116
+ case 'client_secret_basic':
117
+ // eslint-disable-next-line new-cap
118
+ return oidc.ClientSecretBasic(settings.client_secret);
119
+ case 'client_secret_post':
120
+ // eslint-disable-next-line new-cap
121
+ return oidc.ClientSecretPost(settings.client_secret);
122
+ default:
123
+ throw new Error(
124
+ `Unsupported token endpoint auth method: ${settings.token_endpoint_auth_method}`);
125
+ }
126
+ };
127
+
103
128
  const buildConfig = async (settings) => {
104
129
  // openid-client@5 defaulted the token endpoint auth method to
105
130
  // `client_secret_basic`; v6 defaults to `client_secret_post`. Pin the
106
- // method explicitly so existing IdP registrations keep working.
107
- const clientAuth = oidc.ClientSecretBasic(settings.client_secret);
131
+ // default method explicitly so existing IdP registrations keep working.
132
+ const clientAuth = getClientAuth(settings);
108
133
  if (settings.issuer) {
109
134
  const config =
110
135
  await discoverConfig(settings.issuer, settings.client_id, clientAuth);
@@ -170,8 +195,7 @@ exports.expressCreateServer = (hookName, {app}) => {
170
195
  }
171
196
  const oidcSession = req.session.ep_openid_connect || {};
172
197
  if (oidcSession.callbackChecks == null) throw new Error('missing authentication checks');
173
- const currentUrl = new URL(req.originalUrl || req.url, settings.base_url);
174
- const tokens = await oidc.authorizationCodeGrant(oidcConfig, currentUrl, {
198
+ const tokens = await oidc.authorizationCodeGrant(oidcConfig, callbackUrlFromRequest(req), {
175
199
  expectedNonce: oidcSession.callbackChecks.nonce,
176
200
  expectedState: oidcSession.callbackChecks.state,
177
201
  pkceCodeVerifier: oidcSession.callbackChecks.code_verifier,
@@ -310,6 +334,7 @@ exports.preAuthorize = (hookName, {req}) => {
310
334
  };
311
335
 
312
336
  exports.exportedForTestingOnly = {
337
+ callbackUrlFromRequest,
313
338
  defaultSettings,
314
339
  validSettings,
315
340
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "3.0.24",
3
+ "version": "3.0.26",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -113,6 +113,16 @@ describe(__filename, function () {
113
113
  });
114
114
  });
115
115
 
116
+ it('callback URL uses configured base URL with request query parameters', async function () {
117
+ const callbackUrl = epOpenidConnect.exportedForTestingOnly.callbackUrlFromRequest({
118
+ originalUrl: '/ep_openid_connect/callback?code=the_code&state=the_state',
119
+ });
120
+ const expectedUrl = new URL('/ep_openid_connect/callback', pluginSettings.base_url);
121
+ expectedUrl.searchParams.set('code', 'the_code');
122
+ expectedUrl.searchParams.set('state', 'the_state');
123
+ assert.equal(callbackUrl.toString(), expectedUrl.toString());
124
+ });
125
+
116
126
  it('normalUser can create and edit a pad', async function () {
117
127
  const padId = common.randomString();
118
128
  const url = new URL(`/p/${padId}`, common.baseUrl).toString();
@@ -125,6 +135,31 @@ describe(__filename, function () {
125
135
  assert.equal(msg.data.readonly, false);
126
136
  });
127
137
 
138
+ describe('token_endpoint_auth_method', function () {
139
+ for (const tokenEndpointAuthMethod of ['client_secret_basic', 'client_secret_post']) {
140
+ it(`supports ${tokenEndpointAuthMethod}`, async function () {
141
+ await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
142
+ ...pluginSettings,
143
+ token_endpoint_auth_method: tokenEndpointAuthMethod,
144
+ }}});
145
+ const padId = common.randomString();
146
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
147
+ const res = await login(agent, issuer, url, 'normalUser');
148
+ assert.equal(res.request.url, url);
149
+ assert.equal(res.status, 200);
150
+ });
151
+ }
152
+
153
+ it('rejects unsupported methods', async function () {
154
+ await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
155
+ ...pluginSettings,
156
+ token_endpoint_auth_method: 'unsupported',
157
+ }}});
158
+ await agent.get(new URL('/ep_openid_connect/login', common.baseUrl))
159
+ .expect(401);
160
+ });
161
+ });
162
+
128
163
  it('noCreate user is unable to create a pad', async function () {
129
164
  const padId = common.randomString();
130
165
  const url = new URL(`/p/${padId}`, common.baseUrl).toString();