Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ RUN yarn install

COPY . /app

CMD ./start
CMD ["./start"]
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -39,5 +39,6 @@
},
"resolutions": {
"koa-passport/passport": "0.5.3"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
96 changes: 68 additions & 28 deletions src/oidc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const { Issuer, Strategy, custom } = require('openid-client');
const { getEksAuthToken, getTemporaryAwsCredentials } = require('./aws');
const {
clientSecret,
Expand All @@ -10,27 +9,47 @@ 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`;
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) => {
Expand Down Expand Up @@ -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);
Expand All @@ -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]
),
});
}

Expand All @@ -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
);

Expand All @@ -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();
};

Expand Down
56 changes: 32 additions & 24 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
37 changes: 15 additions & 22 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down