@depup/express-openid-connect 2.19.4-depup.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/LICENSE +22 -0
- package/README.md +37 -0
- package/changes.json +34 -0
- package/index.d.ts +1055 -0
- package/index.js +9 -0
- package/lib/appSession.js +420 -0
- package/lib/client.js +167 -0
- package/lib/config.js +326 -0
- package/lib/context.js +498 -0
- package/lib/cookies.js +3 -0
- package/lib/crypto.js +119 -0
- package/lib/debug.js +2 -0
- package/lib/hooks/backchannelLogout/isLoggedOut.js +22 -0
- package/lib/hooks/backchannelLogout/onLogIn.js +21 -0
- package/lib/hooks/backchannelLogout/onLogoutToken.js +37 -0
- package/lib/hooks/getLoginState.js +51 -0
- package/lib/once.js +19 -0
- package/lib/transientHandler.js +155 -0
- package/lib/utils/promisifyCompat.js +90 -0
- package/lib/weakCache.js +8 -0
- package/middleware/attemptSilentLogin.js +66 -0
- package/middleware/auth.js +129 -0
- package/middleware/requiresAuth.js +133 -0
- package/middleware/unauthorizedHandler.js +15 -0
- package/package.json +129 -0
package/index.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
const { strict: assert, AssertionError } = require('assert');
|
|
2
|
+
const {
|
|
3
|
+
JWE,
|
|
4
|
+
errors: { JOSEError },
|
|
5
|
+
} = require('jose');
|
|
6
|
+
const safePromisify = require('./utils/promisifyCompat');
|
|
7
|
+
const cookie = require('cookie');
|
|
8
|
+
const onHeaders = require('on-headers');
|
|
9
|
+
const COOKIES = require('./cookies');
|
|
10
|
+
const { getKeyStore, verifyCookie, signCookie } = require('./crypto');
|
|
11
|
+
const debug = require('./debug')('appSession');
|
|
12
|
+
|
|
13
|
+
const epoch = () => (Date.now() / 1000) | 0;
|
|
14
|
+
const MAX_COOKIE_SIZE = 4096;
|
|
15
|
+
|
|
16
|
+
const REASSIGN = Symbol('reassign');
|
|
17
|
+
const REGENERATED_SESSION_ID = Symbol('regenerated_session_id');
|
|
18
|
+
|
|
19
|
+
function attachSessionObject(req, sessionName, value) {
|
|
20
|
+
Object.defineProperty(req, sessionName, {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get() {
|
|
23
|
+
return value;
|
|
24
|
+
},
|
|
25
|
+
set(arg) {
|
|
26
|
+
if (arg === null || arg === undefined || arg[REASSIGN]) {
|
|
27
|
+
value = arg;
|
|
28
|
+
} else {
|
|
29
|
+
throw new TypeError('session object cannot be reassigned');
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function regenerateSessionStoreId(req, config) {
|
|
37
|
+
if (config.session.store) {
|
|
38
|
+
req[REGENERATED_SESSION_ID] = await config.session.genid(req);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function replaceSession(req, session, config) {
|
|
43
|
+
session[REASSIGN] = true;
|
|
44
|
+
req[config.session.name] = session;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = (config) => {
|
|
48
|
+
const alg = 'dir';
|
|
49
|
+
const enc = 'A256GCM';
|
|
50
|
+
const sessionName = config.session.name;
|
|
51
|
+
const cookieConfig = config.session.cookie;
|
|
52
|
+
const {
|
|
53
|
+
genid: generateId,
|
|
54
|
+
absoluteDuration,
|
|
55
|
+
rolling: rollingEnabled,
|
|
56
|
+
rollingDuration,
|
|
57
|
+
signSessionStoreCookie,
|
|
58
|
+
requireSignedSessionStoreCookie,
|
|
59
|
+
} = config.session;
|
|
60
|
+
|
|
61
|
+
const { transient: emptyTransient, ...emptyCookieOptions } = cookieConfig;
|
|
62
|
+
emptyCookieOptions.expires = emptyTransient ? 0 : new Date();
|
|
63
|
+
emptyCookieOptions.path = emptyCookieOptions.path || '/';
|
|
64
|
+
|
|
65
|
+
const emptyCookie = cookie.serialize(
|
|
66
|
+
`${sessionName}.0`,
|
|
67
|
+
'',
|
|
68
|
+
emptyCookieOptions,
|
|
69
|
+
);
|
|
70
|
+
const cookieChunkSize = MAX_COOKIE_SIZE - emptyCookie.length;
|
|
71
|
+
|
|
72
|
+
let [current, keystore] = getKeyStore(config.secret, true);
|
|
73
|
+
if (keystore.size === 1) {
|
|
74
|
+
keystore = current;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function encrypt(payload, headers) {
|
|
78
|
+
return JWE.encrypt(payload, current, { alg, enc, ...headers });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function decrypt(jwe) {
|
|
82
|
+
return JWE.decrypt(jwe, keystore, {
|
|
83
|
+
complete: true,
|
|
84
|
+
contentEncryptionAlgorithms: [enc],
|
|
85
|
+
keyManagementAlgorithms: [alg],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function calculateExp(iat, uat) {
|
|
90
|
+
if (!rollingEnabled) {
|
|
91
|
+
return iat + absoluteDuration;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!absoluteDuration) {
|
|
95
|
+
return uat + rollingDuration;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Math.min(uat + rollingDuration, iat + absoluteDuration);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setCookie(
|
|
102
|
+
req,
|
|
103
|
+
res,
|
|
104
|
+
{ uat = epoch(), iat = uat, exp = calculateExp(iat, uat) },
|
|
105
|
+
) {
|
|
106
|
+
const cookies = req[COOKIES];
|
|
107
|
+
const { transient: cookieTransient, ...cookieOptions } = cookieConfig;
|
|
108
|
+
cookieOptions.expires = cookieTransient ? 0 : new Date(exp * 1000);
|
|
109
|
+
|
|
110
|
+
// session was deleted or is empty, this matches all session cookies (chunked or unchunked)
|
|
111
|
+
// and clears them, essentially cleaning up what we've set in the past that is now trash
|
|
112
|
+
if (!req[sessionName] || !Object.keys(req[sessionName]).length) {
|
|
113
|
+
debug(
|
|
114
|
+
'session was deleted or is empty, clearing all matching session cookies',
|
|
115
|
+
);
|
|
116
|
+
for (const cookieName of Object.keys(cookies)) {
|
|
117
|
+
if (cookieName.match(`^${sessionName}(?:\\.\\d)?$`)) {
|
|
118
|
+
clearCookie(cookieName, res);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
debug(
|
|
123
|
+
'found session, creating signed session cookie(s) with name %o(.i)',
|
|
124
|
+
sessionName,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const value = encrypt(JSON.stringify(req[sessionName]), {
|
|
128
|
+
iat,
|
|
129
|
+
uat,
|
|
130
|
+
exp,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const chunkCount = Math.ceil(value.length / cookieChunkSize);
|
|
134
|
+
|
|
135
|
+
if (chunkCount > 1) {
|
|
136
|
+
debug('cookie size greater than %d, chunking', cookieChunkSize);
|
|
137
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
138
|
+
const chunkValue = value.slice(
|
|
139
|
+
i * cookieChunkSize,
|
|
140
|
+
(i + 1) * cookieChunkSize,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const chunkCookieName = `${sessionName}.${i}`;
|
|
144
|
+
res.cookie(chunkCookieName, chunkValue, cookieOptions);
|
|
145
|
+
}
|
|
146
|
+
if (sessionName in cookies) {
|
|
147
|
+
debug('replacing non chunked cookie with chunked cookies');
|
|
148
|
+
clearCookie(sessionName, res);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
res.cookie(sessionName, value, cookieOptions);
|
|
152
|
+
for (const cookieName of Object.keys(cookies)) {
|
|
153
|
+
debug('replacing chunked cookies with non chunked cookies');
|
|
154
|
+
if (cookieName.match(`^${sessionName}\\.\\d$`)) {
|
|
155
|
+
clearCookie(cookieName, res);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clearCookie(name, res) {
|
|
163
|
+
const { domain, path, sameSite, secure } = cookieConfig;
|
|
164
|
+
res.clearCookie(name, {
|
|
165
|
+
domain,
|
|
166
|
+
path,
|
|
167
|
+
sameSite,
|
|
168
|
+
secure,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class CookieStore {
|
|
173
|
+
async get(idOrVal) {
|
|
174
|
+
const { protected: header, cleartext } = decrypt(idOrVal);
|
|
175
|
+
return {
|
|
176
|
+
header,
|
|
177
|
+
data: JSON.parse(cleartext),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getCookie(req) {
|
|
182
|
+
return req[COOKIES][sessionName];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setCookie(req, res, iat) {
|
|
186
|
+
setCookie(req, res, iat);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class CustomStore {
|
|
191
|
+
constructor(store) {
|
|
192
|
+
this._get = safePromisify(store.get, store);
|
|
193
|
+
this._set = safePromisify(store.set, store);
|
|
194
|
+
this._destroy = safePromisify(store.destroy, store);
|
|
195
|
+
|
|
196
|
+
let [current, keystore] = getKeyStore(config.secret);
|
|
197
|
+
if (keystore.size === 1) {
|
|
198
|
+
keystore = current;
|
|
199
|
+
}
|
|
200
|
+
this._keyStore = keystore;
|
|
201
|
+
this._current = current;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async get(id) {
|
|
205
|
+
return this._get(id);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async set(
|
|
209
|
+
id,
|
|
210
|
+
req,
|
|
211
|
+
res,
|
|
212
|
+
{ uat = epoch(), iat = uat, exp = calculateExp(iat, uat) },
|
|
213
|
+
) {
|
|
214
|
+
const hasPrevSession = !!req[COOKIES][sessionName];
|
|
215
|
+
const replacingPrevSession = !!req[REGENERATED_SESSION_ID];
|
|
216
|
+
const hasCurrentSession =
|
|
217
|
+
req[sessionName] && Object.keys(req[sessionName]).length;
|
|
218
|
+
if (hasPrevSession && (replacingPrevSession || !hasCurrentSession)) {
|
|
219
|
+
await this._destroy(id);
|
|
220
|
+
}
|
|
221
|
+
if (hasCurrentSession) {
|
|
222
|
+
await this._set(req[REGENERATED_SESSION_ID] || id, {
|
|
223
|
+
header: { iat, uat, exp },
|
|
224
|
+
data: req[sessionName],
|
|
225
|
+
cookie: {
|
|
226
|
+
expires: exp * 1000,
|
|
227
|
+
maxAge: exp * 1000 - Date.now(),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getCookie(req) {
|
|
234
|
+
if (signSessionStoreCookie) {
|
|
235
|
+
const verified = verifyCookie(
|
|
236
|
+
sessionName,
|
|
237
|
+
req[COOKIES][sessionName],
|
|
238
|
+
this._keyStore,
|
|
239
|
+
);
|
|
240
|
+
if (requireSignedSessionStoreCookie) {
|
|
241
|
+
return verified;
|
|
242
|
+
}
|
|
243
|
+
return verified || req[COOKIES][sessionName];
|
|
244
|
+
}
|
|
245
|
+
return req[COOKIES][sessionName];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
setCookie(
|
|
249
|
+
id,
|
|
250
|
+
req,
|
|
251
|
+
res,
|
|
252
|
+
{ uat = epoch(), iat = uat, exp = calculateExp(iat, uat) },
|
|
253
|
+
) {
|
|
254
|
+
if (!req[sessionName] || !Object.keys(req[sessionName]).length) {
|
|
255
|
+
if (req[COOKIES][sessionName]) {
|
|
256
|
+
clearCookie(sessionName, res);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const cookieOptions = {
|
|
260
|
+
...cookieConfig,
|
|
261
|
+
expires: cookieConfig.transient ? 0 : new Date(exp * 1000),
|
|
262
|
+
};
|
|
263
|
+
delete cookieOptions.transient;
|
|
264
|
+
let value = id;
|
|
265
|
+
if (signSessionStoreCookie) {
|
|
266
|
+
value = signCookie(sessionName, id, this._current);
|
|
267
|
+
}
|
|
268
|
+
res.cookie(sessionName, value, cookieOptions);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isCustomStore = !!config.session.store;
|
|
274
|
+
const store = isCustomStore
|
|
275
|
+
? new CustomStore(config.session.store)
|
|
276
|
+
: new CookieStore();
|
|
277
|
+
|
|
278
|
+
return async (req, res, next) => {
|
|
279
|
+
if (req.hasOwnProperty(sessionName)) {
|
|
280
|
+
debug(
|
|
281
|
+
'request object (req) already has %o property, this is indicative of a middleware setup problem',
|
|
282
|
+
sessionName,
|
|
283
|
+
);
|
|
284
|
+
return next(
|
|
285
|
+
new Error(
|
|
286
|
+
`req[${sessionName}] is already set, did you run this middleware twice?`,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
req[COOKIES] = cookie.parse(req.get('cookie') || '');
|
|
292
|
+
|
|
293
|
+
let iat;
|
|
294
|
+
let uat;
|
|
295
|
+
let exp;
|
|
296
|
+
let existingSessionValue;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
if (req[COOKIES].hasOwnProperty(sessionName)) {
|
|
300
|
+
// get JWE from unchunked session cookie
|
|
301
|
+
debug('reading session from %s cookie', sessionName);
|
|
302
|
+
existingSessionValue = store.getCookie(req);
|
|
303
|
+
} else if (req[COOKIES].hasOwnProperty(`${sessionName}.0`)) {
|
|
304
|
+
// get JWE from chunked session cookie
|
|
305
|
+
// iterate all cookie names
|
|
306
|
+
// match and filter for the ones that match sessionName.<number>
|
|
307
|
+
// sort by chunk index
|
|
308
|
+
// concat
|
|
309
|
+
existingSessionValue = Object.entries(req[COOKIES])
|
|
310
|
+
.map(([cookie, value]) => {
|
|
311
|
+
const match = cookie.match(`^${sessionName}\\.(\\d+)$`);
|
|
312
|
+
if (match) {
|
|
313
|
+
return [match[1], value];
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.sort(([a], [b]) => {
|
|
318
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
319
|
+
})
|
|
320
|
+
.map(([i, chunk]) => {
|
|
321
|
+
debug('reading session chunk from %s.%d cookie', sessionName, i);
|
|
322
|
+
return chunk;
|
|
323
|
+
})
|
|
324
|
+
.join('');
|
|
325
|
+
}
|
|
326
|
+
if (existingSessionValue) {
|
|
327
|
+
const sessionData = await store.get(existingSessionValue);
|
|
328
|
+
|
|
329
|
+
// Handle case where store.get() returns undefined/null due to Redis replication lag
|
|
330
|
+
// or race conditions in multi-instance deployments
|
|
331
|
+
if (!sessionData || typeof sessionData !== 'object') {
|
|
332
|
+
debug(
|
|
333
|
+
'session data not found or invalid, treating as expired session',
|
|
334
|
+
);
|
|
335
|
+
// Skip to creating new session - this will be handled by the code after the try block
|
|
336
|
+
} else {
|
|
337
|
+
const { header, data } = sessionData;
|
|
338
|
+
|
|
339
|
+
// Ensure header exists and has required properties
|
|
340
|
+
if (!header || typeof header !== 'object') {
|
|
341
|
+
debug(
|
|
342
|
+
'session header missing or invalid, treating as expired session',
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
({ iat, uat, exp } = header);
|
|
346
|
+
|
|
347
|
+
// check that the existing session isn't expired based on options when it was established
|
|
348
|
+
assert(
|
|
349
|
+
exp > epoch(),
|
|
350
|
+
'it is expired based on options when it was established',
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// check that the existing session isn't expired based on current rollingDuration rules
|
|
354
|
+
if (rollingDuration) {
|
|
355
|
+
assert(
|
|
356
|
+
uat + rollingDuration > epoch(),
|
|
357
|
+
'it is expired based on current rollingDuration rules',
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// check that the existing session isn't expired based on current absoluteDuration rules
|
|
362
|
+
if (absoluteDuration) {
|
|
363
|
+
assert(
|
|
364
|
+
iat + absoluteDuration > epoch(),
|
|
365
|
+
'it is expired based on current absoluteDuration rules',
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
attachSessionObject(req, sessionName, data);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (err instanceof AssertionError) {
|
|
375
|
+
debug('existing session was rejected because', err.message);
|
|
376
|
+
} else if (err instanceof JOSEError) {
|
|
377
|
+
debug(
|
|
378
|
+
'existing session was rejected because it could not be decrypted',
|
|
379
|
+
err,
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
debug('unexpected error handling session', err);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!req.hasOwnProperty(sessionName) || !req[sessionName]) {
|
|
387
|
+
attachSessionObject(req, sessionName, {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (isCustomStore) {
|
|
391
|
+
const id = existingSessionValue || (await generateId(req));
|
|
392
|
+
|
|
393
|
+
onHeaders(res, () =>
|
|
394
|
+
store.setCookie(req[REGENERATED_SESSION_ID] || id, req, res, { iat }),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const { end: origEnd } = res;
|
|
398
|
+
res.end = async function resEnd(...args) {
|
|
399
|
+
try {
|
|
400
|
+
await store.set(id, req, res, {
|
|
401
|
+
iat,
|
|
402
|
+
});
|
|
403
|
+
origEnd.call(res, ...args);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
// need to restore the original `end` so that it gets
|
|
406
|
+
// called after `next(e)` calls the express error handling mw
|
|
407
|
+
res.end = origEnd;
|
|
408
|
+
process.nextTick(() => next(e));
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
} else {
|
|
412
|
+
onHeaders(res, () => store.setCookie(req, res, { iat }));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return next();
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
module.exports.regenerateSessionStoreId = regenerateSessionStoreId;
|
|
420
|
+
module.exports.replaceSession = replaceSession;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const { Issuer, custom } = require('openid-client');
|
|
2
|
+
const url = require('url');
|
|
3
|
+
const urlJoin = require('url-join');
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
const debug = require('./debug')('client');
|
|
6
|
+
const { JWK } = require('jose');
|
|
7
|
+
|
|
8
|
+
const telemetryHeader = {
|
|
9
|
+
name: 'express-oidc',
|
|
10
|
+
version: pkg.version,
|
|
11
|
+
env: {
|
|
12
|
+
node: process.version,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function sortSpaceDelimitedString(string) {
|
|
17
|
+
return string.split(' ').sort().join(' ');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function get(config) {
|
|
21
|
+
const defaultHttpOptions = (options) => {
|
|
22
|
+
options.headers = {
|
|
23
|
+
...options.headers,
|
|
24
|
+
'User-Agent': config.httpUserAgent || `${pkg.name}/${pkg.version}`,
|
|
25
|
+
...(config.enableTelemetry
|
|
26
|
+
? {
|
|
27
|
+
'Auth0-Client': Buffer.from(
|
|
28
|
+
JSON.stringify(telemetryHeader)
|
|
29
|
+
).toString('base64'),
|
|
30
|
+
}
|
|
31
|
+
: undefined),
|
|
32
|
+
};
|
|
33
|
+
options.timeout = config.httpTimeout;
|
|
34
|
+
options.agent = config.httpAgent;
|
|
35
|
+
return options;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const applyHttpOptionsCustom = (entity) =>
|
|
39
|
+
(entity[custom.http_options] = defaultHttpOptions);
|
|
40
|
+
|
|
41
|
+
applyHttpOptionsCustom(Issuer);
|
|
42
|
+
const issuer = await Issuer.discover(config.issuerBaseURL);
|
|
43
|
+
applyHttpOptionsCustom(issuer);
|
|
44
|
+
|
|
45
|
+
const issuerTokenAlgs = Array.isArray(
|
|
46
|
+
issuer.id_token_signing_alg_values_supported
|
|
47
|
+
)
|
|
48
|
+
? issuer.id_token_signing_alg_values_supported
|
|
49
|
+
: [];
|
|
50
|
+
if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) {
|
|
51
|
+
debug(
|
|
52
|
+
'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.',
|
|
53
|
+
config.idTokenSigningAlg,
|
|
54
|
+
issuerTokenAlgs
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const configRespType = sortSpaceDelimitedString(
|
|
59
|
+
config.authorizationParams.response_type
|
|
60
|
+
);
|
|
61
|
+
const issuerRespTypes = Array.isArray(issuer.response_types_supported)
|
|
62
|
+
? issuer.response_types_supported
|
|
63
|
+
: [];
|
|
64
|
+
issuerRespTypes.map(sortSpaceDelimitedString);
|
|
65
|
+
if (!issuerRespTypes.includes(configRespType)) {
|
|
66
|
+
debug(
|
|
67
|
+
'Response type %o is not supported by the issuer. ' +
|
|
68
|
+
'Supported response types are: %o.',
|
|
69
|
+
configRespType,
|
|
70
|
+
issuerRespTypes
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const configRespMode = config.authorizationParams.response_mode;
|
|
75
|
+
const issuerRespModes = Array.isArray(issuer.response_modes_supported)
|
|
76
|
+
? issuer.response_modes_supported
|
|
77
|
+
: [];
|
|
78
|
+
if (configRespMode && !issuerRespModes.includes(configRespMode)) {
|
|
79
|
+
debug(
|
|
80
|
+
'Response mode %o is not supported by the issuer. ' +
|
|
81
|
+
'Supported response modes are %o.',
|
|
82
|
+
configRespMode,
|
|
83
|
+
issuerRespModes
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
config.pushedAuthorizationRequests &&
|
|
89
|
+
!issuer.pushed_authorization_request_endpoint
|
|
90
|
+
) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
'pushed_authorization_request_endpoint must be configured on the issuer to use pushedAuthorizationRequests'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let jwks;
|
|
97
|
+
if (config.clientAssertionSigningKey) {
|
|
98
|
+
const jwk = JWK.asKey(config.clientAssertionSigningKey).toJWK(true);
|
|
99
|
+
jwks = { keys: [jwk] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const client = new issuer.Client(
|
|
103
|
+
{
|
|
104
|
+
client_id: config.clientID,
|
|
105
|
+
client_secret: config.clientSecret,
|
|
106
|
+
id_token_signed_response_alg: config.idTokenSigningAlg,
|
|
107
|
+
token_endpoint_auth_method: config.clientAuthMethod,
|
|
108
|
+
...(config.clientAssertionSigningAlg && {
|
|
109
|
+
token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg,
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
jwks
|
|
113
|
+
);
|
|
114
|
+
applyHttpOptionsCustom(client);
|
|
115
|
+
client[custom.clock_tolerance] = config.clockTolerance;
|
|
116
|
+
|
|
117
|
+
if (config.idpLogout) {
|
|
118
|
+
if (
|
|
119
|
+
config.auth0Logout ||
|
|
120
|
+
(url.parse(issuer.issuer).hostname.match('\\.auth0\\.com$') &&
|
|
121
|
+
config.auth0Logout !== false)
|
|
122
|
+
) {
|
|
123
|
+
Object.defineProperty(client, 'endSessionUrl', {
|
|
124
|
+
value(params) {
|
|
125
|
+
const { id_token_hint, post_logout_redirect_uri, ...extraParams } =
|
|
126
|
+
params;
|
|
127
|
+
const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout'));
|
|
128
|
+
parsedUrl.query = {
|
|
129
|
+
...extraParams,
|
|
130
|
+
returnTo: post_logout_redirect_uri,
|
|
131
|
+
client_id: client.client_id,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
Object.entries(parsedUrl.query).forEach(([key, value]) => {
|
|
135
|
+
if (value === null || value === undefined) {
|
|
136
|
+
delete parsedUrl.query[key];
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return url.format(parsedUrl);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
} else if (!issuer.end_session_endpoint) {
|
|
144
|
+
debug('the issuer does not support RP-Initiated Logout');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { client, issuer };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cache = new Map();
|
|
152
|
+
let timestamp = 0;
|
|
153
|
+
|
|
154
|
+
exports.get = (config) => {
|
|
155
|
+
const { discoveryCacheMaxAge: cacheMaxAge } = config;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (cache.has(config) && now < timestamp + cacheMaxAge) {
|
|
158
|
+
return cache.get(config);
|
|
159
|
+
}
|
|
160
|
+
timestamp = now;
|
|
161
|
+
const promise = get(config).catch((e) => {
|
|
162
|
+
cache.delete(config);
|
|
163
|
+
throw e;
|
|
164
|
+
});
|
|
165
|
+
cache.set(config, promise);
|
|
166
|
+
return promise;
|
|
167
|
+
};
|