@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.
@@ -0,0 +1,51 @@
1
+ const base64url = require('base64url');
2
+ const debug = require('../debug')('getLoginState');
3
+
4
+ /**
5
+ * Generate the state value for use during login transactions. It is used to store the intended
6
+ * return URL after the user authenticates. State is not used to carry unique PRNG values here
7
+ * because the library utilizes either nonce or PKCE for CSRF protection.
8
+ *
9
+ * @param {RequestHandler} req
10
+ * @param {object} options
11
+ *
12
+ * @return {object}
13
+ */
14
+ function defaultState(req, options) {
15
+ const state = { returnTo: options.returnTo || req.originalUrl };
16
+ debug('adding default state %O', state);
17
+ return state;
18
+ }
19
+
20
+ /**
21
+ * Prepare a state object to send.
22
+ *
23
+ * @param {object} stateObject
24
+ *
25
+ * @return {string}
26
+ */
27
+ function encodeState(stateObject = {}) {
28
+ // this filters out nonce, code_verifier, and max_age from the state object so that the values are
29
+ // only stored in its dedicated transient cookie
30
+ const { nonce, code_verifier, max_age, ...filteredState } = stateObject;
31
+ return base64url.encode(JSON.stringify(filteredState));
32
+ }
33
+
34
+ /**
35
+ * Decode a state value.
36
+ *
37
+ * @param {string} stateValue
38
+ *
39
+ * @return {object}
40
+ */
41
+ function decodeState(stateValue) {
42
+ try {
43
+ return JSON.parse(base64url.decode(stateValue));
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ module.exports.defaultState = defaultState;
50
+ module.exports.encodeState = encodeState;
51
+ module.exports.decodeState = decodeState;
package/lib/once.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Given a callback, return a wrapped callback that will only run once regardless of calls.
3
+ */
4
+ function once(callback) {
5
+ let called = false;
6
+ let value;
7
+
8
+ return (...args) => {
9
+ if (!called) {
10
+ value = callback(...args);
11
+ }
12
+
13
+ called = true;
14
+
15
+ return value;
16
+ };
17
+ }
18
+
19
+ module.exports = { once };
@@ -0,0 +1,155 @@
1
+ const { generators } = require('openid-client');
2
+ const {
3
+ signCookie: generateCookieValue,
4
+ verifyCookie: getCookieValue,
5
+ getKeyStore,
6
+ } = require('./crypto');
7
+ const COOKIES = require('./cookies');
8
+
9
+ class TransientCookieHandler {
10
+ constructor({ secret, session, legacySameSiteCookie }) {
11
+ let [current, keystore] = getKeyStore(secret);
12
+
13
+ if (keystore.size === 1) {
14
+ keystore = current;
15
+ }
16
+ this.currentKey = current;
17
+ this.keyStore = keystore;
18
+ this.sessionCookieConfig = (session && session.cookie) || {};
19
+ this.legacySameSiteCookie = legacySameSiteCookie;
20
+ }
21
+
22
+ /**
23
+ * Set a cookie with a value or a generated nonce.
24
+ *
25
+ * @param {String} key Cookie name to use.
26
+ * @param {Object} req Express Request object.
27
+ * @param {Object} res Express Response object.
28
+ * @param {Object} opts Options object.
29
+ * @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None."
30
+ * @param {String} opts.value Cookie value. Omit this key to store a generated value.
31
+ * @param {Boolean} opts.legacySameSiteCookie Should a fallback cookie be set? Default is true.
32
+ *
33
+ * @return {String} Cookie value that was set.
34
+ */
35
+ store(
36
+ key,
37
+ req,
38
+ res,
39
+ { sameSite = 'None', value = this.generateNonce() } = {}
40
+ ) {
41
+ const isSameSiteNone = sameSite === 'None';
42
+ const { domain, path, secure } = this.sessionCookieConfig;
43
+ const basicAttr = {
44
+ httpOnly: true,
45
+ secure,
46
+ domain,
47
+ path,
48
+ };
49
+
50
+ {
51
+ const cookieValue = generateCookieValue(key, value, this.currentKey);
52
+ // Set the cookie with the SameSite attribute and, if needed, the Secure flag.
53
+ res.cookie(key, cookieValue, {
54
+ ...basicAttr,
55
+ sameSite,
56
+ secure: isSameSiteNone ? true : basicAttr.secure,
57
+ });
58
+ }
59
+
60
+ if (isSameSiteNone && this.legacySameSiteCookie) {
61
+ const cookieValue = generateCookieValue(
62
+ `_${key}`,
63
+ value,
64
+ this.currentKey
65
+ );
66
+ // Set the fallback cookie with no SameSite or Secure attributes.
67
+ res.cookie(`_${key}`, cookieValue, basicAttr);
68
+ }
69
+
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * Get a cookie value then delete it.
75
+ *
76
+ * @param {String} key Cookie name to use.
77
+ * @param {Object} req Express Request object.
78
+ * @param {Object} res Express Response object.
79
+ *
80
+ * @return {String|undefined} Cookie value or undefined if cookie was not found.
81
+ */
82
+ getOnce(key, req, res) {
83
+ if (!req[COOKIES]) {
84
+ return undefined;
85
+ }
86
+
87
+ const { secure, sameSite } = this.sessionCookieConfig;
88
+
89
+ let value = getCookieValue(key, req[COOKIES][key], this.keyStore);
90
+ this.deleteCookie(key, res, { secure, sameSite });
91
+
92
+ if (this.legacySameSiteCookie) {
93
+ const fallbackKey = `_${key}`;
94
+ if (!value) {
95
+ value = getCookieValue(
96
+ fallbackKey,
97
+ req[COOKIES][fallbackKey],
98
+ this.keyStore
99
+ );
100
+ }
101
+ this.deleteCookie(fallbackKey, res);
102
+ }
103
+
104
+ return value;
105
+ }
106
+
107
+ /**
108
+ * Generates a nonce value.
109
+ *
110
+ * @return {String}
111
+ */
112
+ generateNonce() {
113
+ return generators.nonce();
114
+ }
115
+
116
+ /**
117
+ * Generates a code_verifier value.
118
+ *
119
+ * @return {String}
120
+ */
121
+ generateCodeVerifier() {
122
+ return generators.codeVerifier();
123
+ }
124
+
125
+ /**
126
+ * Calculates a code_challenge value for a given codeVerifier
127
+ *
128
+ * @param {String} codeVerifier Code Verifier to calculate the code_challenge value from.
129
+ *
130
+ * @return {String}
131
+ */
132
+ calculateCodeChallenge(codeVerifier) {
133
+ return generators.codeChallenge(codeVerifier);
134
+ }
135
+
136
+ /**
137
+ * Clears the cookie from the browser by setting an empty value and an expiration date in the past
138
+ *
139
+ * @param {String} name Cookie name
140
+ * @param {Object} res Express Response object
141
+ * @param {Object?} opts Optional SameSite and Secure cookie options for modern browsers
142
+ */
143
+ deleteCookie(name, res, opts = {}) {
144
+ const { domain, path } = this.sessionCookieConfig;
145
+ const { sameSite, secure } = opts;
146
+ res.clearCookie(name, {
147
+ domain,
148
+ path,
149
+ sameSite,
150
+ secure,
151
+ });
152
+ }
153
+ }
154
+
155
+ module.exports = TransientCookieHandler;
@@ -0,0 +1,90 @@
1
+ const utilPromisify = require('util-promisify');
2
+
3
+ /**
4
+ * Checks if a method follows callback-based pattern
5
+ * @param {Function} method - The method to check
6
+ * @returns {boolean} True if method appears to be callback-based
7
+ */
8
+ function isCallbackBasedMethod(method) {
9
+ const paramCount = method.length;
10
+
11
+ // Must have at least 2 parameters for callback pattern
12
+ if (paramCount < 2) {
13
+ return false;
14
+ }
15
+
16
+ const methodSource = method.toString();
17
+ const callbackPatterns = ['cb', 'callback'];
18
+
19
+ return callbackPatterns.some((pattern) => methodSource.includes(pattern));
20
+ }
21
+
22
+ /**
23
+ * Checks if a method is declared as async
24
+ * @param {Function} method - The method to check
25
+ * @returns {boolean} True if method is async
26
+ */
27
+ function isAsyncFunction(method) {
28
+ return method.constructor.name === 'AsyncFunction';
29
+ }
30
+ /**
31
+ * Safely promisify store methods to avoid Node.js deprecation warnings.
32
+ * Uses util-promisify with additional safety checks to prevent hanging.
33
+ *
34
+ * @param {Function} method - The method to potentially promisify
35
+ * @param {Object} context - The context to bind the method to
36
+ * @returns {Function} Promise-based function
37
+ * @throws {TypeError} When method is not a function
38
+ */
39
+ function safePromisify(method, context) {
40
+ if (typeof method !== 'function') {
41
+ throw new TypeError('Expected method to be a function');
42
+ }
43
+
44
+ // 1. Async functions - require callbacks
45
+ if (isAsyncFunction(method) && isCallbackBasedMethod(method)) {
46
+ return function (...args) {
47
+ return new Promise((resolve, reject) => {
48
+ // Add callback as last argument
49
+ const callback = (err, result) => {
50
+ if (err) reject(err);
51
+ else resolve(result);
52
+ };
53
+ method.call(context, ...args, callback);
54
+ });
55
+ };
56
+ }
57
+ // 2. Async functions - these are already Promise-based
58
+ if (isAsyncFunction(method)) {
59
+ return method.bind(context);
60
+ }
61
+
62
+ // 3. Functions that clearly return Promises (without calling them)
63
+ const methodSource = method.toString();
64
+ const directPromisePatterns = [
65
+ 'return Promise.resolve',
66
+ 'return Promise.reject',
67
+ 'return new Promise',
68
+ ];
69
+
70
+ const returnsDirectPromise = directPromisePatterns.some((pattern) =>
71
+ methodSource.includes(pattern),
72
+ );
73
+
74
+ if (returnsDirectPromise) {
75
+ return method.bind(context);
76
+ }
77
+
78
+ // 3. For all other cases, use util-promisify safely
79
+ // util-promisify should handle:
80
+ // - Callback-based methods: promisifies them without deprecation warnings
81
+ // - Promise-returning methods: returns them as-is
82
+ try {
83
+ return utilPromisify(method).bind(context);
84
+ } catch {
85
+ // Fallback to treating as already Promise-based if util-promisify fails
86
+ return method.bind(context);
87
+ }
88
+ }
89
+
90
+ module.exports = safePromisify;
@@ -0,0 +1,8 @@
1
+ const map = new WeakMap();
2
+
3
+ function instance(ctx) {
4
+ if (!map.has(ctx)) map.set(ctx, {});
5
+ return map.get(ctx);
6
+ }
7
+
8
+ module.exports = instance;
@@ -0,0 +1,66 @@
1
+ const debug = require('../lib/debug')('attemptSilentLogin');
2
+ const COOKIES = require('../lib/cookies');
3
+ const weakRef = require('../lib/weakCache');
4
+
5
+ const COOKIE_NAME = 'skipSilentLogin';
6
+
7
+ const cancelSilentLogin = (req, res) => {
8
+ const {
9
+ config: {
10
+ session: {
11
+ cookie: { secure, domain, path, sameSite },
12
+ },
13
+ },
14
+ } = weakRef(req.oidc);
15
+ res.cookie(COOKIE_NAME, true, {
16
+ httpOnly: true,
17
+ secure,
18
+ domain,
19
+ path,
20
+ sameSite,
21
+ });
22
+ };
23
+
24
+ const resumeSilentLogin = (req, res) => {
25
+ const {
26
+ config: {
27
+ session: {
28
+ cookie: { domain, path, sameSite, secure },
29
+ },
30
+ },
31
+ } = weakRef(req.oidc);
32
+ res.clearCookie(COOKIE_NAME, {
33
+ httpOnly: true,
34
+ domain,
35
+ path,
36
+ sameSite,
37
+ secure,
38
+ });
39
+ };
40
+
41
+ module.exports = function attemptSilentLogin() {
42
+ return (req, res, next) => {
43
+ if (!req.oidc) {
44
+ next(
45
+ new Error('req.oidc is not found, did you include the auth middleware?')
46
+ );
47
+ return;
48
+ }
49
+
50
+ const silentLoginAttempted = !!(req[COOKIES] || {})[COOKIE_NAME];
51
+
52
+ if (
53
+ !silentLoginAttempted &&
54
+ !req.oidc.isAuthenticated() &&
55
+ req.accepts('html')
56
+ ) {
57
+ debug('Attempting silent login');
58
+ cancelSilentLogin(req, res);
59
+ return res.oidc.silentLogin();
60
+ }
61
+ next();
62
+ };
63
+ };
64
+
65
+ module.exports.cancelSilentLogin = cancelSilentLogin;
66
+ module.exports.resumeSilentLogin = resumeSilentLogin;
@@ -0,0 +1,129 @@
1
+ const express = require('express');
2
+
3
+ const debug = require('../lib/debug')('auth');
4
+ const { get: getConfig } = require('../lib/config');
5
+ const { requiresAuth } = require('./requiresAuth');
6
+ const attemptSilentLogin = require('./attemptSilentLogin');
7
+ const TransientCookieHandler = require('../lib/transientHandler');
8
+ const { RequestContext, ResponseContext } = require('../lib/context');
9
+ const appSession = require('../lib/appSession');
10
+ const isLoggedOut = require('../lib/hooks/backchannelLogout/isLoggedOut');
11
+
12
+ const enforceLeadingSlash = (path) => {
13
+ return path.split('')[0] === '/' ? path : '/' + path;
14
+ };
15
+
16
+ /**
17
+ * Returns a router with two routes /login and /callback
18
+ *
19
+ * @param {Object} [params] The parameters object; see index.d.ts for types and descriptions.
20
+ *
21
+ * @returns {express.Router} the router
22
+ */
23
+ const auth = function (params) {
24
+ const config = getConfig(params);
25
+ debug('configuration object processed, resulting configuration: %O', config);
26
+ const router = new express.Router();
27
+ const transient = new TransientCookieHandler(config);
28
+
29
+ router.use(appSession(config));
30
+
31
+ // Express context and OpenID Issuer discovery.
32
+ router.use(async (req, res, next) => {
33
+ req.oidc = new RequestContext(config, req, res, next);
34
+ res.oidc = new ResponseContext(config, req, res, next, transient);
35
+ next();
36
+ });
37
+
38
+ // Login route, configurable with routes.login
39
+ if (config.routes.login) {
40
+ const path = enforceLeadingSlash(config.routes.login);
41
+ debug('adding GET %s route', path);
42
+ router.get(path, express.urlencoded({ extended: false }), (req, res) =>
43
+ res.oidc.login({ returnTo: config.baseURL })
44
+ );
45
+ } else {
46
+ debug('login handling route not applied');
47
+ }
48
+
49
+ // Logout route, configurable with routes.logout
50
+ if (config.routes.logout) {
51
+ const path = enforceLeadingSlash(config.routes.logout);
52
+ debug('adding GET %s route', path);
53
+ router.get(path, (req, res) => res.oidc.logout());
54
+ } else {
55
+ debug('logout handling route not applied');
56
+ }
57
+
58
+ // Callback route, configured with routes.callback.
59
+ if (config.routes.callback) {
60
+ const path = enforceLeadingSlash(config.routes.callback);
61
+ debug('adding GET %s route', path);
62
+ router.get(path, (req, res) => res.oidc.callback());
63
+ debug('adding POST %s route', path);
64
+ router.post(path, express.urlencoded({ extended: false }), (req, res) =>
65
+ res.oidc.callback()
66
+ );
67
+ } else {
68
+ debug('callback handling route not applied');
69
+ }
70
+
71
+ if (config.backchannelLogout) {
72
+ const path = enforceLeadingSlash(config.routes.backchannelLogout);
73
+ debug('adding POST %s route', path);
74
+ router.post(path, express.urlencoded({ extended: false }), (req, res) =>
75
+ res.oidc.backchannelLogout()
76
+ );
77
+
78
+ if (config.backchannelLogout.isLoggedOut !== false) {
79
+ const isLoggedOutFn = config.backchannelLogout.isLoggedOut || isLoggedOut;
80
+ router.use(async (req, res, next) => {
81
+ if (!req.oidc.isAuthenticated()) {
82
+ next();
83
+ return;
84
+ }
85
+ try {
86
+ const loggedOut = await isLoggedOutFn(req, config);
87
+ if (loggedOut) {
88
+ req[config.session.name] = undefined;
89
+ }
90
+ next();
91
+ } catch (e) {
92
+ next(e);
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ if (config.authRequired) {
99
+ debug(
100
+ 'authentication is required for all routes this middleware is applied to'
101
+ );
102
+ router.use(requiresAuth());
103
+ } else {
104
+ debug(
105
+ 'authentication is not required for any of the routes this middleware is applied to ' +
106
+ 'see and apply `requiresAuth` middlewares to your protected resources'
107
+ );
108
+ }
109
+ if (config.attemptSilentLogin) {
110
+ debug("silent login will be attempted on end-user's initial HTML request");
111
+ router.use(attemptSilentLogin());
112
+ }
113
+
114
+ return router;
115
+ };
116
+
117
+ /**
118
+ * Used for instantiating a custom session store. eg
119
+ *
120
+ * ```js
121
+ * const { auth } = require('express-openid-connect');
122
+ * const MemoryStore = require('memorystore')(auth);
123
+ * ```
124
+ *
125
+ * @constructor
126
+ */
127
+ auth.Store = function () {};
128
+
129
+ module.exports = auth;
@@ -0,0 +1,133 @@
1
+ const createError = require('http-errors');
2
+ const debug = require('../lib/debug')('requiresAuth');
3
+
4
+ const defaultRequiresLogin = (req) => !req.oidc.isAuthenticated();
5
+
6
+ /**
7
+ * Returns a middleware that checks whether an end-user is authenticated.
8
+ * If end-user is not authenticated `res.oidc.login()` is triggered for an HTTP
9
+ * request that can perform a redirect.
10
+ */
11
+ async function requiresLoginMiddleware(requiresLoginCheck, req, res, next) {
12
+ if (!req.oidc) {
13
+ next(
14
+ new Error('req.oidc is not found, did you include the auth middleware?')
15
+ );
16
+ return;
17
+ }
18
+
19
+ if (requiresLoginCheck(req)) {
20
+ if (!res.oidc.errorOnRequiredAuth && req.accepts('html')) {
21
+ debug(
22
+ 'authentication requirements not met with errorOnRequiredAuth() returning false, calling res.oidc.login()'
23
+ );
24
+ return res.oidc.login();
25
+ }
26
+ debug(
27
+ 'authentication requirements not met with errorOnRequiredAuth() returning true, calling next() with an Unauthorized error'
28
+ );
29
+ next(
30
+ createError.Unauthorized('Authentication is required for this route.')
31
+ );
32
+ return;
33
+ }
34
+
35
+ debug('authentication requirements met, calling next()');
36
+
37
+ next();
38
+ }
39
+
40
+ module.exports.requiresAuth = function requiresAuth(
41
+ requiresLoginCheck = defaultRequiresLogin
42
+ ) {
43
+ return requiresLoginMiddleware.bind(undefined, requiresLoginCheck);
44
+ };
45
+
46
+ function checkJSONprimitive(value) {
47
+ if (
48
+ typeof value !== 'string' &&
49
+ typeof value !== 'number' &&
50
+ typeof value !== 'boolean' &&
51
+ value !== null
52
+ ) {
53
+ throw new TypeError('"expected" must be a string, number, boolean or null');
54
+ }
55
+ }
56
+
57
+ module.exports.claimEquals = function claimEquals(claim, expected) {
58
+ // check that claim is a string value
59
+ if (typeof claim !== 'string') {
60
+ throw new TypeError('"claim" must be a string');
61
+ }
62
+ // check that expected is a JSON supported primitive
63
+ checkJSONprimitive(expected);
64
+
65
+ const authenticationCheck = (req) => {
66
+ if (defaultRequiresLogin(req)) {
67
+ return true;
68
+ }
69
+ const { idTokenClaims } = req.oidc;
70
+ if (!(claim in idTokenClaims)) {
71
+ return true;
72
+ }
73
+ const actual = idTokenClaims[claim];
74
+ if (actual !== expected) {
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ };
80
+ return requiresLoginMiddleware.bind(undefined, authenticationCheck);
81
+ };
82
+
83
+ module.exports.claimIncludes = function claimIncludes(claim, ...expected) {
84
+ // check that claim is a string value
85
+ if (typeof claim !== 'string') {
86
+ throw new TypeError('"claim" must be a string');
87
+ }
88
+ // check that all expected are JSON supported primitives
89
+ expected.forEach(checkJSONprimitive);
90
+
91
+ const authenticationCheck = (req) => {
92
+ if (defaultRequiresLogin(req)) {
93
+ return true;
94
+ }
95
+ const { idTokenClaims } = req.oidc;
96
+ if (!(claim in idTokenClaims)) {
97
+ return true;
98
+ }
99
+
100
+ let actual = idTokenClaims[claim];
101
+ if (typeof actual === 'string') {
102
+ actual = actual.split(' ');
103
+ } else if (!Array.isArray(actual)) {
104
+ debug(
105
+ 'unexpected claim type. expected array or string, got %o',
106
+ typeof actual
107
+ );
108
+ return true;
109
+ }
110
+
111
+ actual = new Set(actual);
112
+
113
+ return !expected.every(Set.prototype.has.bind(actual));
114
+ };
115
+ return requiresLoginMiddleware.bind(undefined, authenticationCheck);
116
+ };
117
+
118
+ module.exports.claimCheck = function claimCheck(func) {
119
+ // check that func is a function
120
+ if (typeof func !== 'function' || func.constructor.name !== 'Function') {
121
+ throw new TypeError('"claimCheck" expects a function');
122
+ }
123
+ const authenticationCheck = (req) => {
124
+ if (defaultRequiresLogin(req)) {
125
+ return true;
126
+ }
127
+
128
+ const { idTokenClaims } = req.oidc;
129
+
130
+ return !func(req, idTokenClaims);
131
+ };
132
+ return requiresLoginMiddleware.bind(undefined, authenticationCheck);
133
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns a middleware that start the login transaction on
3
+ * Unauthorized errors (i.e. errors with statusCode === 401)
4
+ *
5
+ * This middleware needs to be included after your application
6
+ * routes.
7
+ */
8
+ module.exports = function () {
9
+ return (err, req, res, next) => {
10
+ if (err.statusCode === 401) {
11
+ return res.oidc.login();
12
+ }
13
+ next(err);
14
+ };
15
+ };