From 73d2aa90e6d1215bc46e98cd6fdbb828f56f948c Mon Sep 17 00:00:00 2001 From: Shaun Lum Date: Mon, 25 Nov 2024 13:38:29 -0800 Subject: [PATCH 1/3] added openid and profile to scope --- backend/src/routes/auth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index ba94301a..29552609 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -41,6 +41,7 @@ router.get( "/login", passport.authenticate("oidc", { failureRedirect: "error", + scope: ['openid','profile'] }) ); From 404ed0725edec1905f3fea51a69926b123f1d04f Mon Sep 17 00:00:00 2001 From: Shaun Lum Date: Wed, 27 Nov 2024 15:27:01 -0800 Subject: [PATCH 2/3] added the idtoken for passport and updated the logout routes to include id_token_hint --- backend/src/app.js | 4 +++- backend/src/routes/auth.js | 47 +++++++++++++++++++------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/backend/src/app.js b/backend/src/app.js index 97de775a..a82b8419 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -106,7 +106,8 @@ utils.getOidcDiscovery().then(discovery => { callbackURL: config.get('server:frontend') + '/api/auth/callback', scope: discovery.scopes_supported, kc_idp_hint: config.get('server:idirIDPHint') - }, (_issuer, _sub, profile, accessToken, refreshToken, done) => { + }, (_issuer, _sub, profile, accessToken, refreshToken, params, done) => { + const idToken = params.id_token; if ((typeof (accessToken) === 'undefined') || (accessToken === null) || (typeof (refreshToken) === 'undefined') || (refreshToken === null)) { return done('No access token', null); @@ -116,6 +117,7 @@ utils.getOidcDiscovery().then(discovery => { profile.jwtFrontend = auth.generateUiToken(); profile.jwt = accessToken; profile.refreshToken = refreshToken; + profile.idToken = idToken; return done(null, profile); })); //JWT strategy is used for authorization diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 29552609..0796ede0 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -55,34 +55,33 @@ function logout(req) { } //removes tokens and destroys session -router.get("/logout", async (req, res) => { - if (req?.session?.passport?.user) { - logout(req); +router.get("/logout", async (req, res, next) => { + let primaryURL = config.get('logoutEndpoint') + '?post_logout_redirect_uri=' + config.get('server:frontend'); + let idToken = req?.session?.passport?.user?.idToken; + if (idToken) { + req.logout(function(err) { + if (err) { + return next(err); + } + req.session.destroy(); + let retUrl; + if (req.query && req.query.sessionExpired) { + retUrl = encodeURIComponent(primaryURL + '/session-expired' + '&id_token_hint=' + idToken); + } else { + retUrl = encodeURIComponent(primaryURL + '/logout' + '&id_token_hint=' + idToken); + } + res.redirect(config.get('siteMinder_logout_endpoint') + retUrl); + }); + }else { let retUrl; if (req.query && req.query.sessionExpired) { - retUrl = encodeURIComponent( - config.get("logoutEndpoint") + - "?post_logout_redirect_uri=" + - config.get("server:frontend") + - "/session-expired" - ); + console.log("1") + retUrl = encodeURIComponent(primaryURL + '/session-expired' + '&client_id=' + config.get('oidc:clientId')); } else { - retUrl = encodeURIComponent( - - config.get("logoutEndpoint") + - "?post_logout_redirect_uri=" + - config.get("server:frontend") + - "/logout" - ); + console.log("2") + retUrl = encodeURIComponent(primaryURL + '/logout' + '&client_id=' + config.get('oidc:clientId')); } - res.redirect(config.get("siteMinder_logout_endpoint") + retUrl); - } else { - res.redirect(config.get("server:frontend") + "/logout"); - // if (req.query && req.query.sessionExpired) { - // res.redirect(config.get("server:frontend") + "/session-expired"); - // } else { - // res.redirect(config.get("server:frontend") + "/logout"); - // } + res.redirect(config.get('siteMinder_logout_endpoint') + retUrl); } }); From c7b8ba02c9c3c446d779f59c2a2ec7d8e4267c9f Mon Sep 17 00:00:00 2001 From: Tang Date: Fri, 6 Dec 2024 13:42:42 -0800 Subject: [PATCH 3/3] added fixes to the 5 min timeout --- backend/src/components/auth.js | 246 +++++++++++++++++++++------------ 1 file changed, 155 insertions(+), 91 deletions(-) diff --git a/backend/src/components/auth.js b/backend/src/components/auth.js index 043db1c3..378acc4f 100644 --- a/backend/src/components/auth.js +++ b/backend/src/components/auth.js @@ -1,52 +1,82 @@ -'use strict'; - -const axios = require('axios'); -const config = require('../config/index'); -const log = require('./logger'); -const jsonwebtoken = require('jsonwebtoken'); -const qs = require('querystring'); -const utils = require('./utils'); -const safeStringify = require('fast-safe-stringify'); -const userRoles = require('./roles'); -const {partial, fromPairs} = require('lodash'); -const HttpStatus = require('http-status-codes'); -const {pick} = require('lodash'); -const {ApiError} = require('./error'); +"use strict"; + +const axios = require("axios"); +const config = require("../config/index"); +const log = require("./logger"); +const jsonwebtoken = require("jsonwebtoken"); +const qs = require("querystring"); +const utils = require("./utils"); +const safeStringify = require("fast-safe-stringify"); +const userRoles = require("./roles"); +const { partial, fromPairs } = require("lodash"); +const HttpStatus = require("http-status-codes"); +const { pick } = require("lodash"); +const { ApiError } = require("./error"); /** * Create help functions for authorization: isValidGMPUserToken, isValidGMPUser, isValidGMPAdmin, etc * @param {*} roles */ function createRoleHelpers(roles) { - const userTokenHelpers = Object.entries(roles.User).map(([roleType, roleNames]) => [ - `isValid${roleType}UserToken`, isValidUiToken(isUserHasRoles, roleType, roleNames) - ]); + const userTokenHelpers = Object.entries(roles.User).map( + ([roleType, roleNames]) => [ + `isValid${roleType}UserToken`, + isValidUiToken(isUserHasRoles, roleType, roleNames), + ] + ); // userHelpers is called to generate object { isValidGMPUser: ture, isValidUMPUser: true, isValidStudentSearchUser: false, ...} which will be passed to the frontend. - const userHelpers = Object.entries(roles.User).map(([roleType, roleNames]) => [ - `isValid${roleType}User`, isValidUser(isUserHasRoles, roleType, roleNames) - ]); - const adminHelpers = Object.entries(roles.Admin).map(([roleType, roleName]) => [ - `isValid${roleType}Admin`, isValidUiToken(isUserHasAdminRole, roleType, roleName) - ]); - const adminHelpersFE = Object.entries(roles.Admin).map(([roleType, roleName]) => [ - `isValid${roleType}Admin`, isValidUser(isUserHasAdminRole, roleType, roleName) - ]); + const userHelpers = Object.entries(roles.User).map( + ([roleType, roleNames]) => [ + `isValid${roleType}User`, + isValidUser(isUserHasRoles, roleType, roleNames), + ] + ); + const adminHelpers = Object.entries(roles.Admin).map( + ([roleType, roleName]) => [ + `isValid${roleType}Admin`, + isValidUiToken(isUserHasAdminRole, roleType, roleName), + ] + ); + const adminHelpersFE = Object.entries(roles.Admin).map( + ([roleType, roleName]) => [ + `isValid${roleType}Admin`, + isValidUser(isUserHasAdminRole, roleType, roleName), + ] + ); // create object { isValidGMPUser: ture, isValidUMPUser: true, isValidStudentSearchUser: false, ...} - const isValidUsers = (req) => fromPairs(userHelpers.map(([roleType, verifyRole]) => [roleType, verifyRole(req)])); - const isValidAdminUsers = (req) => fromPairs(adminHelpersFE.map(([roleType, verifyRole]) => [roleType, verifyRole(req)])); - return ({...fromPairs([...userTokenHelpers, ...userHelpers, ...adminHelpers]), isValidUsers, isValidAdminUsers}); + const isValidUsers = (req) => + fromPairs( + userHelpers.map(([roleType, verifyRole]) => [roleType, verifyRole(req)]) + ); + const isValidAdminUsers = (req) => + fromPairs( + adminHelpersFE.map(([roleType, verifyRole]) => [ + roleType, + verifyRole(req), + ]) + ); + return { + ...fromPairs([...userTokenHelpers, ...userHelpers, ...adminHelpers]), + isValidUsers, + isValidAdminUsers, + }; } function isUserHasAdminRole(roleType, roleName, roles) { - const adminRole = roleName || ''; + const adminRole = roleName || ""; log.silly(`valid ${roleType} from environment variable is ${adminRole}`); return !!(Array.isArray(roles) && roles.includes(adminRole)); } function isUserHasRoles(roleType, roleNames, roles) { const validRoles = roleNames || []; - log.silly(`valid ${roleType} Roles from environment variable are ${safeStringify(validRoles)}`); - const isValidUserRole = (element) => Array.isArray(validRoles) ? validRoles.includes(element) : false; + log.silly( + `valid ${roleType} Roles from environment variable are ${safeStringify( + validRoles + )}` + ); + const isValidUserRole = (element) => + Array.isArray(validRoles) ? validRoles.includes(element) : false; return !!(Array.isArray(roles) && roles.some(isValidUserRole)); } @@ -56,40 +86,53 @@ function isValidUiToken(isUserHasRole, roleType, roleNames) { const jwtToken = utils.getBackendToken(req); if (!jwtToken) { return res.status(HttpStatus.UNAUTHORIZED).json({ - message: 'Unauthorized user' + message: "Unauthorized user", }); } let userToken; try { - userToken = jsonwebtoken.verify(jwtToken, config.get('oidc:publicKey')); + userToken = jsonwebtoken.verify(jwtToken, config.get("oidc:publicKey")); } catch (e) { - log.debug('error is from verify', e); + log.debug("error is from verify", e); return res.status(HttpStatus.UNAUTHORIZED).json(); } - if (userToken['realm_access'] && userToken['realm_access'].roles - && isUserHasRole(roleType, roleNames, userToken['realm_access'].roles)) { + if ( + userToken["realm_access"] && + userToken["realm_access"].roles && + isUserHasRole(roleType, roleNames, userToken["realm_access"].roles) + ) { return next(); } return res.status(HttpStatus.FORBIDDEN).json({ - message: 'user is missing role' + message: "user is missing role", }); } catch (e) { log.error(e); return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(); } - }; } function isValidUser(isUserHasRole, roleType, roleNames) { return function isValidUserHandler(req) { - try { - const thisSession = req['session']; - if (thisSession && thisSession['passport'] && thisSession['passport'].user && thisSession['passport'].user.jwt) { - const userToken = jsonwebtoken.verify(thisSession['passport'].user.jwt, config.get('oidc:publicKey')); - if (userToken && userToken.realm_access && userToken.realm_access.roles - && (isUserHasRole(roleType, roleNames, userToken.realm_access.roles))) { + const thisSession = req["session"]; + if ( + thisSession && + thisSession["passport"] && + thisSession["passport"].user && + thisSession["passport"].user.jwt + ) { + const userToken = jsonwebtoken.verify( + thisSession["passport"].user.jwt, + config.get("oidc:publicKey") + ); + if ( + userToken && + userToken.realm_access && + userToken.realm_access.roles && + isUserHasRole(roleType, roleNames, userToken.realm_access.roles) + ) { return true; } } @@ -109,7 +152,7 @@ const auth = { const now = Date.now().valueOf() / 1000; const payload = jsonwebtoken.decode(token); - return (!!payload['exp'] && payload['exp'] < (now + 30)); // Add 30 seconds to make sure , edge case is avoided and token is refreshed. + return !!payload["exp"] && payload["exp"] < now + 30; // Add 30 seconds to make sure , edge case is avoided and token is refreshed. }, // Check if JWT Refresh Token has expired @@ -118,11 +161,14 @@ const auth = { const payload = jsonwebtoken.decode(token); // Check if expiration exists, or lacks expiration - if (typeof (payload['exp']) !== 'undefined' && payload['exp'] !== null) { - return payload['exp'] === 0 || payload['exp'] > now; - } else if (typeof (payload['iat']) !== 'undefined' && payload['iat'] !== null) { - const expiresIn = config.get('tokenGenerate:expiresIn') || '1800' - return payload['iat'] + parseInt(expiresIn) > now; + if (typeof payload["exp"] !== "undefined" && payload["exp"] !== null) { + return payload["exp"] === 0 || payload["exp"] > now; + } else if ( + typeof payload["iat"] !== "undefined" && + payload["iat"] !== null + ) { + const expiresIn = config.get("tokenGenerate:expiresIn") || "1800"; + return payload["iat"] + parseInt(expiresIn) > now; } else { return false; } @@ -134,25 +180,27 @@ const auth = { try { const discovery = await utils.getOidcDiscovery(); - const response = await axios.post(discovery.token_endpoint, + const response = await axios.post( + discovery.token_endpoint, qs.stringify({ - client_id: config.get('oidc:clientId'), - client_secret: config.get('oidc:clientSecret'), - grant_type: 'refresh_token', + client_id: config.get("oidc:clientId"), + client_secret: config.get("oidc:clientSecret"), + grant_type: "refresh_token", refresh_token: refreshToken, - scope: discovery.scopes_supported - }), { + scope: "openid profile", + }), + { headers: { - Accept: 'application/json', - 'Cache-Control': 'no-cache', - 'Content-Type': 'application/x-www-form-urlencoded', - } + Accept: "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/x-www-form-urlencoded", + }, } ); result.jwt = response.data.access_token; result.refreshToken = response.data.refresh_token; } catch (error) { - log.error('renew', error.message); + log.error("renew", error.message); result = error.response.data; } @@ -163,47 +211,50 @@ const auth = { async refreshJWT(req, _res, next) { try { if (req?.user?.jwt) { - log.verbose('refreshJWT', 'User & JWT exists'); + log.verbose("refreshJWT", "User & JWT exists"); if (auth.isTokenExpired(req.user.jwt)) { - log.verbose('refreshJWT', 'JWT has expired'); + log.verbose("refreshJWT", "JWT has expired"); - if (req?.user?.refreshToken && auth.isRenewable(req.user.refreshToken)) { - log.verbose('refreshJWT', 'Can refresh JWT token'); + if ( + req?.user?.refreshToken && + auth.isRenewable(req.user.refreshToken) + ) { + log.verbose("refreshJWT", "Can refresh JWT token"); // Get new JWT and Refresh Tokens and update the request const result = await auth.renew(req.user.refreshToken); req.user.jwt = result.jwt; // eslint-disable-line require-atomic-updates req.user.refreshToken = result.refreshToken; // eslint-disable-line require-atomic-updates } else { - log.verbose('refreshJWT', 'Cannot refresh JWT token'); + log.verbose("refreshJWT", "Cannot refresh JWT token"); delete req.user; } } } else { - log.verbose('refreshJWT', 'No existing User or JWT'); + log.verbose("refreshJWT", "No existing User or JWT"); delete req.user; } } catch (error) { - log.error('refreshJWT', error.message); + log.error("refreshJWT", error.message); } next(); }, generateUiToken() { - const i = config.get('tokenGenerate:issuer'); - const s = 'user@penrequest.ca'; - const a = config.get('server:frontend'); + const i = config.get("tokenGenerate:issuer"); + const s = "user@penrequest.ca"; + const a = config.get("server:frontend"); const signOptions = { issuer: i, subject: s, audience: a, - expiresIn: '30m', - algorithm: 'RS256' + expiresIn: "30m", + algorithm: "RS256", }; - const privateKey = config.get('tokenGenerate:privateKey'); + const privateKey = config.get("tokenGenerate:privateKey"); const uiToken = jsonwebtoken.sign({}, privateKey, signOptions); - + return uiToken; }, isValidUiTokenWithRoles: partial(isValidUiToken, isUserHasRoles), @@ -213,33 +264,46 @@ const auth = { async getApiCredentials() { try { const discovery = await utils.getOidcDiscovery(); - const response = await axios.post(discovery.token_endpoint, + const response = await axios.post( + discovery.token_endpoint, qs.stringify({ - client_id: config.get('oidc:clientId'), - client_secret: config.get('oidc:clientSecret'), - grant_type: 'client_credentials', - scope: discovery.scopes_supported - }), { + client_id: config.get("oidc:clientId"), + client_secret: config.get("oidc:clientSecret"), + grant_type: "client_credentials", + scope: discovery.scopes_supported, + }), + { headers: { - Accept: 'application/json', - 'Cache-Control': 'no-cache', - 'Content-Type': 'application/x-www-form-urlencoded', - } + Accept: "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/x-www-form-urlencoded", + }, } ); - log.verbose('getApiCredentials Res', safeStringify(response.data)); + log.verbose("getApiCredentials Res", safeStringify(response.data)); let result = {}; result.accessToken = response.data.access_token; result.refreshToken = response.data.refresh_token; return result; } catch (error) { - log.error('getApiCredentials Error', error.response ? pick(error.response, ['status', 'statusText', 'data']) : error.message); - const status = error.response ? error.response.status : HttpStatus.INTERNAL_SERVER_ERROR; - throw new ApiError(status, {message: 'Get getApiCredentials error'}, error); + log.error( + "getApiCredentials Error", + error.response + ? pick(error.response, ["status", "statusText", "data"]) + : error.message + ); + const status = error.response + ? error.response.status + : HttpStatus.INTERNAL_SERVER_ERROR; + throw new ApiError( + status, + { message: "Get getApiCredentials error" }, + error + ); } - } + }, }; module.exports = auth;