ep_openid_connect 3.0.20 → 3.0.22

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.
Files changed (2) hide show
  1. package/index.js +90 -54
  2. package/package.json +5 -5
package/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const Ajv = require('ajv/dist/jtd');
4
4
  const {URL} = require('url');
5
- const {Issuer, generators} = require('openid-client');
6
5
 
7
6
  let logger = {};
8
7
  for (const level of ['debug', 'info', 'warn', 'error']) {
@@ -14,7 +13,14 @@ const defaultSettings = {
14
13
  user_properties: {},
15
14
  };
16
15
  let settings;
17
- let oidcClient = null;
16
+ let oidcConfig = null;
17
+ // openid-client@6 is ESM-only, so it must be loaded with a dynamic import from
18
+ // this CommonJS module. The reference is cached after the first call.
19
+ let oidc = null;
20
+ const loadOidc = async () => {
21
+ if (oidc == null) oidc = await import('openid-client');
22
+ return oidc;
23
+ };
18
24
 
19
25
  const validSettings = new Ajv().compile({
20
26
  properties: {
@@ -53,23 +59,35 @@ const validateSubClaim = (sub) => {
53
59
  }
54
60
  };
55
61
 
56
- const discoverIssuer = async (issuerUrl) => {
57
- issuerUrl = new URL(issuerUrl);
62
+ const isHttp = (urlString) => {
63
+ try {
64
+ return new URL(urlString).protocol === 'http:';
65
+ } catch (e) {
66
+ return false;
67
+ }
68
+ };
69
+
70
+ const discoverConfig = async (issuerUrl, clientId, clientAuth) => {
71
+ const url = new URL(issuerUrl);
58
72
  // https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery says that the URI
59
73
  // must not have query or fragment components.
60
- if (issuerUrl.search) {
61
- throw new Error(`Unexpected query in issuer URL (${issuerUrl}): ${issuerUrl.search}`);
74
+ if (url.search) {
75
+ throw new Error(`Unexpected query in issuer URL (${url}): ${url.search}`);
62
76
  }
63
- if (issuerUrl.hash) {
64
- throw new Error(`Unexpected fragment in issuer URL (${issuerUrl}): ${issuerUrl.hash}`);
77
+ if (url.hash) {
78
+ throw new Error(`Unexpected fragment in issuer URL (${url}): ${url.hash}`);
65
79
  }
66
- let issuer;
80
+ // openid-client@6 rejects http:// issuers by default; opt back in for
81
+ // localhost / private-network providers (matches v5 behaviour).
82
+ const options = url.protocol === 'http:'
83
+ ? {execute: [oidc.allowInsecureRequests]}
84
+ : undefined;
67
85
  try {
68
- issuer = await Issuer.discover(issuerUrl.href);
86
+ return await oidc.discovery(url, clientId, undefined, clientAuth, options);
69
87
  } catch (err) {
70
88
  // The URL used to get the issuer metadata doesn't exactly follow RFC 8615; see:
71
89
  // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
72
- const discoveryUrl = new URL(issuerUrl);
90
+ const discoveryUrl = new URL(url);
73
91
  if (!discoveryUrl.pathname.includes('/.well-known/')) {
74
92
  discoveryUrl.pathname =
75
93
  `${discoveryUrl.pathname.replace(/\/$/, '')}/.well-known/openid-configuration`;
@@ -80,22 +98,36 @@ const discoverIssuer = async (issuerUrl) => {
80
98
  `Does your issuer support Discovery? (hint: ${discoveryUrl})`);
81
99
  throw err;
82
100
  }
83
- logger.info('OpenID Connect Discovery complete.');
84
- return issuer;
85
101
  };
86
102
 
87
- const getIssuer = async (settings) => {
88
- if (settings.issuer) return await discoverIssuer(settings.issuer);
89
- return new Issuer(settings.issuer_metadata);
103
+ const buildConfig = async (settings) => {
104
+ // openid-client@5 defaulted the token endpoint auth method to
105
+ // `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);
108
+ if (settings.issuer) {
109
+ const config =
110
+ await discoverConfig(settings.issuer, settings.client_id, clientAuth);
111
+ logger.info('OpenID Connect Discovery complete.');
112
+ return config;
113
+ }
114
+ const config =
115
+ new oidc.Configuration(settings.issuer_metadata, settings.client_id, undefined, clientAuth);
116
+ if (isHttp(settings.issuer_metadata && settings.issuer_metadata.issuer)) {
117
+ oidc.allowInsecureRequests(config);
118
+ }
119
+ return config;
90
120
  };
91
121
 
92
122
  exports.init_ep_openid_connect = async (hookName, {logger: l}) => {
93
123
  if (l != null) logger = l;
124
+ await loadOidc();
94
125
  };
95
126
 
96
127
  exports.loadSettings = async (hookName, {settings: {ep_openid_connect: s = {}}}) => {
97
- oidcClient = null;
128
+ oidcConfig = null;
98
129
  settings = null;
130
+ await loadOidc();
99
131
  if (!validSettings(s)) {
100
132
  logger.error('Invalid settings. Detailed validation errors:', validSettings.errors);
101
133
  return;
@@ -121,12 +153,7 @@ exports.loadSettings = async (hookName, {settings: {ep_openid_connect: s = {}}})
121
153
  // Make sure base_url ends with '/' so that relative URLs are appended:
122
154
  if (!settings.base_url.endsWith('/')) settings.base_url += '/';
123
155
  logger.debug('Settings:', {...settings, client_secret: '********'});
124
- oidcClient = new (await getIssuer(settings)).Client({
125
- client_id: settings.client_id,
126
- client_secret: settings.client_secret,
127
- response_types: ['code'],
128
- redirect_uris: [endpointUrl('callback')],
129
- });
156
+ oidcConfig = await buildConfig(settings);
130
157
  logger.info('Configured.');
131
158
  };
132
159
 
@@ -137,16 +164,22 @@ exports.expressCreateServer = (hookName, {app}) => {
137
164
  // otherwise the user could be caught in an infinite redirect loop.
138
165
  try {
139
166
  logger.debug(`Processing ${req.url}`);
140
- if (oidcClient == null) {
167
+ if (oidcConfig == null) {
141
168
  logger.warn('Not configured; ignoring request.');
142
169
  return next();
143
170
  }
144
- const params = oidcClient.callbackParams(req);
145
171
  const oidcSession = req.session.ep_openid_connect || {};
146
172
  if (oidcSession.callbackChecks == null) throw new Error('missing authentication checks');
147
- const tokenset =
148
- await oidcClient.callback(endpointUrl('callback'), params, oidcSession.callbackChecks);
149
- const userinfo = await oidcClient.userinfo(tokenset);
173
+ const currentUrl = new URL(req.originalUrl || req.url, settings.base_url);
174
+ const tokens = await oidc.authorizationCodeGrant(oidcConfig, currentUrl, {
175
+ expectedNonce: oidcSession.callbackChecks.nonce,
176
+ expectedState: oidcSession.callbackChecks.state,
177
+ pkceCodeVerifier: oidcSession.callbackChecks.code_verifier,
178
+ idTokenExpected: true,
179
+ });
180
+ const claims = tokens.claims();
181
+ const userinfo =
182
+ await oidc.fetchUserInfo(oidcConfig, tokens.access_token, claims && claims.sub);
150
183
  validateSubClaim(userinfo.sub);
151
184
  // The user has successfully authenticated, but don't set req.session.user here -- do it in
152
185
  // the authenticate hook so that Etherpad can log the authentication success. However, DO "log
@@ -165,33 +198,36 @@ exports.expressCreateServer = (hookName, {app}) => {
165
198
  return next(err);
166
199
  }
167
200
  });
168
- app.get(ep('login'), (req, res, next) => {
169
- logger.debug(`Processing ${req.url}`);
170
- if (oidcClient == null) {
171
- logger.warn('Not configured; ignoring request.');
172
- return next();
201
+ app.get(ep('login'), async (req, res, next) => {
202
+ try {
203
+ logger.debug(`Processing ${req.url}`);
204
+ if (oidcConfig == null) {
205
+ logger.warn('Not configured; ignoring request.');
206
+ return next();
207
+ }
208
+ if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
209
+ const oidcSession = req.session.ep_openid_connect;
210
+ const code_verifier = oidc.randomPKCECodeVerifier(); // RFC7636
211
+ const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier);
212
+ const nonce = oidc.randomNonce();
213
+ const state = oidc.randomState();
214
+ oidcSession.callbackChecks = {nonce, state, code_verifier};
215
+ const url = oidc.buildAuthorizationUrl(oidcConfig, {
216
+ redirect_uri: endpointUrl('callback'),
217
+ scope: settings.scope.join(' '),
218
+ nonce,
219
+ state,
220
+ code_challenge,
221
+ code_challenge_method: 'S256',
222
+ });
223
+ res.redirect(303, url.toString());
224
+ } catch (err) {
225
+ return next(err);
173
226
  }
174
- if (req.session.ep_openid_connect == null) req.session.ep_openid_connect = {};
175
- const oidcSession = req.session.ep_openid_connect;
176
- const commonParams = {
177
- nonce: generators.nonce(),
178
- scope: settings.scope.join(' '),
179
- state: generators.state(),
180
- };
181
- oidcSession.callbackChecks = {
182
- ...commonParams,
183
- code_verifier: generators.codeVerifier(), // RFC7636
184
- };
185
- res.redirect(303, oidcClient.authorizationUrl({
186
- ...commonParams,
187
- // RFC7636
188
- code_challenge: generators.codeChallenge(oidcSession.callbackChecks.code_verifier),
189
- code_challenge_method: 'S256',
190
- }));
191
227
  });
192
228
  app.get(ep('logout'), (req, res, next) => {
193
229
  logger.debug(`Processing ${req.url}`);
194
- if (oidcClient == null) {
230
+ if (oidcConfig == null) {
195
231
  logger.warn('Not configured; ignoring request.');
196
232
  return next();
197
233
  }
@@ -200,7 +236,7 @@ exports.expressCreateServer = (hookName, {app}) => {
200
236
  };
201
237
 
202
238
  exports.authenticate = (hookName, {req, res, users}) => {
203
- if (oidcClient == null) return;
239
+ if (oidcConfig == null) return;
204
240
  logger.debug('authenticate hook for', req.url);
205
241
  const {ep_openid_connect: {userinfo} = {}} = req.session;
206
242
  if (userinfo == null) { // Nullish means the user isn't authenticated.
@@ -228,7 +264,7 @@ exports.authenticate = (hookName, {req, res, users}) => {
228
264
  };
229
265
 
230
266
  exports.authnFailure = (hookName, {req, res}) => {
231
- if (oidcClient == null) return;
267
+ if (oidcConfig == null) return;
232
268
  // Normally the user is redirected to the login page which would then redirect the user back once
233
269
  // authenticated. For non-GET requests, send a 401 instead because users can't be redirected back.
234
270
  // Also send a 401 if an Authorization header is present to facilitate API error handling.
@@ -268,7 +304,7 @@ exports.authnFailure = (hookName, {req, res}) => {
268
304
  };
269
305
 
270
306
  exports.preAuthorize = (hookName, {req}) => {
271
- if (oidcClient == null) return;
307
+ if (oidcConfig == null) return;
272
308
  if (req.path.startsWith(ep(''))) return true;
273
309
  return;
274
310
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "3.0.20",
3
+ "version": "3.0.22",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -23,15 +23,15 @@
23
23
  },
24
24
  "homepage": "https://github.com/ether/ep_openid_connect#readme",
25
25
  "dependencies": {
26
- "ajv": "^8.18.0",
27
- "openid-client": "^5.7.1"
26
+ "ajv": "^8.20.0",
27
+ "openid-client": "^6.8.4"
28
28
  },
29
29
  "devDependencies": {
30
30
  "eslint": "^8.57.1",
31
31
  "eslint-config-etherpad": "^4.0.5",
32
- "oidc-provider": "^8.8.1",
32
+ "oidc-provider": "^9.8.3",
33
33
  "supertest": "^6.3.4",
34
- "typescript": "^5.9.3"
34
+ "typescript": "^6.0.3"
35
35
  },
36
36
  "scripts": {
37
37
  "lint": "eslint .",