From eac20ebabf77d9575ac1a9fa193937c099deab3b Mon Sep 17 00:00:00 2001 From: Rajendra Pandey Date: Tue, 19 May 2026 15:32:07 -0700 Subject: [PATCH 1/2] Use Node 20 Docker image and bypass Yarn engine checks --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9643323..29ba754 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,8 @@ RUN yarn global add pm2 -g COPY package.json yarn.lock /app/ -RUN yarn install +RUN yarn install --ignore-engines COPY . /app -CMD ./start +CMD ["./start"] From b5f71851067498fe278e7b49ad2d63f4893856ec Mon Sep 17 00:00:00 2001 From: Rajendra Pandey Date: Tue, 19 May 2026 15:39:31 -0700 Subject: [PATCH 2/2] Upgrade openid-client to v6 and remove yarn engine bypass --- Dockerfile | 2 +- package.json | 5 +-- src/oidc.js | 96 ++++++++++++++++++++++++++++++++++++--------------- src/router.js | 56 +++++++++++++++++------------- yarn.lock | 37 ++++++++------------ 5 files changed, 119 insertions(+), 77 deletions(-) diff --git a/Dockerfile b/Dockerfile index 29ba754..4f4f848 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN yarn global add pm2 -g COPY package.json yarn.lock /app/ -RUN yarn install --ignore-engines +RUN yarn install COPY . /app diff --git a/package.json b/package.json index 557ba7f..04db34d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "koa-router": "^12.0.0", "koa-session": "^6.2.0", "lru-cache": "^7.13.1", - "openid-client": "^5.1.8", + "openid-client": "^6.8.4", "shelljs": "^0.8.5", "winston": "^3.8.1", "yargs": "^17.5.1" @@ -39,5 +39,6 @@ }, "resolutions": { "koa-passport/passport": "0.5.3" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/oidc.js b/src/oidc.js index 1af03e1..837cb7c 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -1,4 +1,3 @@ -const { Issuer, Strategy, custom } = require('openid-client'); const { getEksAuthToken, getTemporaryAwsCredentials } = require('./aws'); const { clientSecret, @@ -10,14 +9,26 @@ const { ignoreEmailVerification, } = require('./config'); -// Set global request timeout for the OIDC library. This is relatively high as some -// providers can take a while to respond. -// https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing-http-requests -custom.setHttpOptionsDefaults({ - timeout: 30000, -}); - let passportStrategy; +let oidcConfig; +let openidClientPromise; +let openidClientPassportPromise; +// eslint-disable-next-line no-new-func +const dynamicImport = new Function('modulePath', 'return import(modulePath)'); + +const getOpenIdClient = async () => { + if (openidClientPromise === undefined) { + openidClientPromise = dynamicImport('openid-client'); + } + return openidClientPromise; +}; + +const getOpenIdClientPassport = async () => { + if (openidClientPassportPromise === undefined) { + openidClientPassportPromise = dynamicImport('openid-client/passport'); + } + return openidClientPassportPromise; +}; const getBasePath = () => `${loginUrl}/oauth`; const getCallbackPath = () => `${getBasePath()}/callback`; @@ -25,12 +36,20 @@ const getRedirectUrl = (ctx) => `${ctx.protocol}://${ctx.host}${getCallbackPath()}`; const getClient = async () => { - const issuer = await Issuer.discover(oidcIssuer); + if (oidcConfig !== undefined) { + return oidcConfig; + } - return new issuer.Client({ - client_id: clientId, - client_secret: clientSecret, - }); + const openidClient = await getOpenIdClient(); + oidcConfig = await openidClient.discovery( + new URL(oidcIssuer), + clientId, + clientSecret, + undefined, + { timeout: 30 } + ); + + return oidcConfig; }; const getAssumeRoleErrorMessage = (error, roleArn) => { @@ -69,8 +88,28 @@ const validateEmail = (userinfo) => { // Take the info returned from the OIDC provider and return a user object // This cannot be an arrow function as we rely on `this` to be the strategy that // calls this function -async function handleAuthenticationSuccess(tokenset, userinfo, done) { +async function handleAuthenticationSuccess(req, tokenset, done) { let awsCredentials; + const openidClient = await getOpenIdClient(); + + const idTokenClaims = tokenset.claims ? tokenset.claims() : {}; + let userinfo = { ...idTokenClaims }; + + if (tokenset.access_token !== undefined) { + const expectedSubject = + idTokenClaims && idTokenClaims.sub + ? idTokenClaims.sub + : openidClient.skipSubjectCheck; + try { + userinfo = await openidClient.fetchUserInfo( + await getClient(), + tokenset.access_token, + expectedSubject + ); + } catch (error) { + return done(error); + } + } // Check the email address const { emailValid, emailError } = validateEmail(userinfo); @@ -83,12 +122,15 @@ async function handleAuthenticationSuccess(tokenset, userinfo, done) { awsCredentials = await getTemporaryAwsCredentials( userinfo.email, tokenset.id_token, - this.iamRole + req.session.selectedIamRole || iamRoles[0] ); } catch (e) { return done(null, false, { error: e, - message: getAssumeRoleErrorMessage(e, this.iamRole || iamRoles[0]), + message: getAssumeRoleErrorMessage( + e, + req.session.selectedIamRole || iamRoles[0] + ), }); } @@ -104,15 +146,11 @@ const getPassportStrategy = async () => { return Promise.resolve(passportStrategy); } - const client = await getClient(); - - const params = { scope: 'openid email' }; - const usePKCE = true; // optional, defaults to false, when true the code_challenge_method will be - // resolved from the issuer configuration, instead of true you may provide - // any of the supported values directly, i.e. "S256" (recommended) or "plain" + const { Strategy } = await getOpenIdClientPassport(); + const config = await getClient(); passportStrategy = new Strategy( - { client, params, usePKCE }, + { config, scope: 'openid email', passReqToCallback: true }, handleAuthenticationSuccess ); @@ -122,15 +160,17 @@ const getPassportStrategy = async () => { // Sets the redirect_uri dynamically based on the host and uses the `iam_role` query parameter // to dynamically set the role to be assumed const dynamicStrategyMiddleware = async (ctx, next) => { - const strategy = await getPassportStrategy(); - // eslint-disable-next-line no-underscore-dangle - strategy._params.redirect_uri = getRedirectUrl(ctx); - + const [defaultIamRole] = iamRoles; const roleIndex = parseInt(ctx.query.iam_role, 10); if (!Number.isNaN(roleIndex)) { - strategy.iamRole = iamRoles[roleIndex]; + const [selectedIamRole] = iamRoles.slice(roleIndex); + ctx.session.selectedIamRole = selectedIamRole || defaultIamRole; + } else { + ctx.session.selectedIamRole = defaultIamRole; } + ctx.state.oidcCallbackUrl = getRedirectUrl(ctx); + await next(); }; diff --git a/src/router.js b/src/router.js index fab15e9..1827054 100644 --- a/src/router.js +++ b/src/router.js @@ -29,39 +29,47 @@ router.get(`${loginUrl}/check`, async (ctx) => { }); // Start OIDC authentication request -router.get( - oidc.getBasePath(), - oidc.dynamicStrategyMiddleware, - passport.authenticate('oidc') +router.get(oidc.getBasePath(), oidc.dynamicStrategyMiddleware, (ctx) => + passport.authenticate('oidc', { + callbackURL: ctx.state.oidcCallbackUrl, + })(ctx) ); // OIDC authentication callback router.get(oidc.getCallbackPath(), (ctx) => - passport.authenticate('oidc', async (error, user, info) => { - // If authentication failed, render an error message - if (error || user === false) { - const renderContext = { - error, - ...info, - loginUrl, - }; + passport.authenticate( + 'oidc', + { + callbackURL: `${ctx.protocol}://${ + ctx.host + }${oidc.getCallbackPath()}`, + }, + async (error, user, info) => { + // If authentication failed, render an error message + if (error || user === false) { + const renderContext = { + error, + ...info, + loginUrl, + }; + + renderContext.message = + renderContext.message || + 'An unexpected error occured while trying to log you in.'; - renderContext.message = - renderContext.message || - 'An unexpected error occured while trying to log you in.'; + if (renderContext.error) { + log.error(renderContext.error); + } - if (renderContext.error) { - log.error(renderContext.error); + await ctx.render('error', renderContext); + return; } - await ctx.render('error', renderContext); - return; + // Otherwise log the user in and redirect to the root URL + await ctx.login(user); + ctx.redirect('/'); } - - // Otherwise log the user in and redirect to the root URL - await ctx.login(user); - ctx.redirect('/'); - })(ctx) + )(ctx) ); module.exports = router; diff --git a/yarn.lock b/yarn.lock index b3b6ff0..19099ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1380,10 +1380,10 @@ jmespath@0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -jose@^4.1.4: - version "4.8.3" - resolved "https://registry.yarnpkg.com/jose/-/jose-4.8.3.tgz#5a754fb4aa5f2806608d083f438e6916b11087da" - integrity sha512-7rySkpW78d8LBp4YU70Wb7+OTgE3OwAALNVZxhoIhp4Kscp+p/fBkdpxGAMKxvCAMV4QfXBU9m6l9nX/vGwd2g== +jose@^6.2.2: + version "6.2.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.3.tgz#0975197ad973251221c658a3cddc4b951a250c2d" + integrity sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw== js-yaml@^4.1.0: version "4.1.0" @@ -1677,16 +1677,16 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +oauth4webapi@^3.8.5: + version "3.8.6" + resolved "https://registry.yarnpkg.com/oauth4webapi/-/oauth4webapi-3.8.6.tgz#0ede466d8be8774db38558a90612c8b6186abba4" + integrity sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ== + object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" - integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== - object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -1725,11 +1725,6 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -oidc-token-hash@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" - integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== - on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -1756,15 +1751,13 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== -openid-client@^5.1.8: - version "5.1.8" - resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.1.8.tgz#3a24910288b32c32f548fb6e391f44178ce6370f" - integrity sha512-EPxJY6bT7YIYQEXSGxRC5flQ3GUhLy98ufdto6+BVBrFGPmwjUpy4xBcYuU/Wt9nPkO/3EgljBrr6Ezx4lp1RQ== +openid-client@^6.8.4: + version "6.8.4" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-6.8.4.tgz#573e852a9c6ea3fcfe180956da6aaf979c4e4724" + integrity sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw== dependencies: - jose "^4.1.4" - lru-cache "^6.0.0" - object-hash "^2.0.1" - oidc-token-hash "^5.0.1" + jose "^6.2.2" + oauth4webapi "^3.8.5" optionator@^0.9.1: version "0.9.1"