ep_openid_connect 2.1.1 → 2.1.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.
Files changed (2) hide show
  1. package/index.js +34 -23
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -19,6 +19,15 @@ let oidcClient = null;
19
19
  const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
20
20
  const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
21
21
 
22
+ const validateSubClaim = (sub) => {
23
+ if (typeof sub !== 'string' || // 'sub' claim must exist as a string per OIDC spec.
24
+ sub === '' || // Empty string doesn't make sense.
25
+ sub === '__proto__' || // Prevent prototype pollution.
26
+ settings.prohibited_usernames.includes(sub)) {
27
+ throw new Error('invalid sub claim');
28
+ }
29
+ };
30
+
22
31
  const discoverIssuer = async (issuerUrl) => {
23
32
  issuerUrl = new URL(issuerUrl);
24
33
  // https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery says that the URI
@@ -98,26 +107,34 @@ exports.clientVars = (hookName, context) => {
98
107
  exports.expressCreateServer = (hookName, {app}) => {
99
108
  if (oidcClient == null) return;
100
109
  logger.debug('Configuring auth routes');
101
- app.get(ep('callback'), (req, res, next) => {
102
- logger.debug(`Processing ${req.url}`);
103
- (async () => {
110
+ app.get(ep('callback'), async (req, res, next) => {
111
+ // This handler MUST NOT redirect to a page that requires authentication if there is a problem,
112
+ // otherwise the user could be caught in an infinite redirect loop.
113
+ try {
114
+ logger.debug(`Processing ${req.url}`);
104
115
  const params = oidcClient.callbackParams(req);
105
116
  const oidcSession = req.session.ep_openid_connect || {};
106
- const {authParams} = oidcSession;
107
- if (authParams == null) throw new Error('no authentication paramters found in session state');
108
- const tokenset = await oidcClient.callback(endpointUrl('callback'), params, authParams);
109
- oidcSession.userinfo = await oidcClient.userinfo(tokenset);
117
+ if (oidcSession.authParams == null) throw new Error('missing authentication paramters');
118
+ const tokenset =
119
+ await oidcClient.callback(endpointUrl('callback'), params, oidcSession.authParams);
120
+ const userinfo = await oidcClient.userinfo(tokenset);
121
+ validateSubClaim(userinfo.sub);
110
122
  // The user has successfully authenticated, but don't set req.session.user here -- do it in
111
123
  // the authenticate hook so that Etherpad can log the authentication success. However, DO "log
112
124
  // out" the previous user to force the authenticate hook to run in case the user was already
113
125
  // authenticated as someone else.
114
126
  delete req.session.user;
127
+ // userinfo should not be stored in req.session until after all checks have passed. (Otherwise
128
+ // it would be too easy to accidentally introduce a vulnerability.)
129
+ oidcSession.userinfo = userinfo;
115
130
  res.redirect(oidcSession.next || settings.base_url);
116
131
  // Defer deletion of state until success so that the user can reload the page to retry after a
117
132
  // transient backchannel failure.
118
133
  delete oidcSession.authParams;
119
134
  delete oidcSession.next;
120
- })().catch(next);
135
+ } catch (err) {
136
+ return next(err);
137
+ }
121
138
  });
122
139
  app.get(ep('login'), (req, res, next) => {
123
140
  logger.debug(`Processing ${req.url}`);
@@ -137,32 +154,26 @@ exports.expressCreateServer = (hookName, {app}) => {
137
154
  exports.authenticate = (hookName, {req, res, users}) => {
138
155
  if (oidcClient == null) return;
139
156
  logger.debug('authenticate hook for', req.url);
140
- const {session} = req;
141
- const {ep_openid_connect: {userinfo = {}} = {}} = session;
142
- const {sub} = userinfo;
143
- if (sub == null || // Nullish means the user isn't authenticated.
144
- typeof sub !== 'string' || // `sub` is used as the username, so it must be a string.
145
- sub === '' || // Empty string doesn't make sense.
146
- sub === '__proto__' || // Prevent prototype pollution.
147
- settings.prohibited_usernames.includes(sub)) {
157
+ const {ep_openid_connect: {userinfo} = {}} = req.session;
158
+ if (userinfo == null) { // Nullish means the user isn't authenticated.
148
159
  // Out of an abundance of caution, clear out the old state, nonce, and userinfo (if present) to
149
160
  // force regeneration.
150
- delete session.ep_openid_connect;
161
+ delete req.session.ep_openid_connect;
151
162
  // Authn failed. Let another plugin try to authenticate the user.
152
163
  return;
153
164
  }
154
165
  // Successfully authenticated.
155
166
  logger.info('Successfully authenticated user with userinfo:', userinfo);
156
- session.user = users[sub];
157
- if (session.user == null) session.user = users[sub] = {};
167
+ req.session.user = users[userinfo.sub];
168
+ if (req.session.user == null) req.session.user = users[userinfo.sub] = {};
158
169
  for (const [propName, descriptor] of Object.entries(settings.user_properties)) {
159
170
  if (descriptor.claim != null && descriptor.claim in userinfo) {
160
- session.user[propName] = userinfo[descriptor.claim];
161
- } else if ('default' in descriptor && !(propName in session.user)) {
162
- session.user[propName] = descriptor.default;
171
+ req.session.user[propName] = userinfo[descriptor.claim];
172
+ } else if ('default' in descriptor && !(propName in req.session.user)) {
173
+ req.session.user[propName] = descriptor.default;
163
174
  }
164
175
  }
165
- logger.debug('User properties:', session.user);
176
+ logger.debug('User properties:', req.session.user);
166
177
  return true;
167
178
  };
168
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {