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.
- package/index.js +34 -23
- 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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
}
|
|
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 {
|
|
141
|
-
|
|
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
|
|