Skip to content

Commit

Permalink
Ofmcc 244 impersonate (#22)
Browse files Browse the repository at this point in the history
* Updating branch with implementation for impersonate functionality.

* Fixed end of line semi-colons not caught by prettier.

* Updates to conform with code review feedback.

* Added unit tests.

---------

Co-authored-by: weskubo-cgi <[email protected]>
  • Loading branch information
jgstorey and weskubo-cgi authored Oct 23, 2023
1 parent e91c2e1 commit e660aa5
Show file tree
Hide file tree
Showing 33 changed files with 1,183 additions and 375 deletions.
174 changes: 88 additions & 86 deletions backend/src/components/auth.js
Original file line number Diff line number Diff line change
@@ -1,171 +1,173 @@
'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 HttpStatus = require('http-status-codes');
const safeStringify = require('fast-safe-stringify');
const {ApiError} = require('./error');
const {pick} = require('lodash');
'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 HttpStatus = require('http-status-codes')
const safeStringify = require('fast-safe-stringify')
const { ApiError } = require('./error')
const { pick } = require('lodash')

const auth = {
// Check if JWT Access Token has expired
isTokenExpired(token) {
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.
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.
},

// Check if JWT Refresh Token has expired
isRenewable(token) {
const now = Date.now().valueOf() / 1000;
const payload = jsonwebtoken.decode(token);
const now = Date.now().valueOf() / 1000
const payload = jsonwebtoken.decode(token)

// Check if expiration exists, or lacks expiration
return (typeof (payload.exp) !== 'undefined' && payload.exp !== null &&
payload.exp === 0 || payload.exp > now);
return (typeof payload.exp !== 'undefined' && payload.exp !== null && payload.exp === 0) || payload.exp > now
},

// Get new JWT and Refresh tokens
async renew(refreshToken, isIdirUser) {
let result = {};
let clientId = isIdirUser? config.get('oidc:clientIdIDIR') : config.get('oidc:clientId');
let clientSecret = isIdirUser? config.get('oidc:clientSecretIDIR') : config.get('oidc:clientSecret');
let result = {}
let clientId = isIdirUser ? config.get('oidc:clientIdIDIR') : config.get('oidc:clientId')
let clientSecret = isIdirUser ? config.get('oidc:clientSecretIDIR') : config.get('oidc:clientSecret')
try {
const discovery = await utils.getOidcDiscovery();
const response = await axios.post(discovery.token_endpoint,
const discovery = await utils.getOidcDiscovery()
const response = await axios.post(
discovery.token_endpoint,
qs.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
scope: 'offline_access'
}), {
scope: 'offline_access',
}),
{
headers: {
Accept: 'application/json',
'Cache-Control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded',
}
}
);
},
},
)

log.verbose('renew', utils.prettyStringify(response.data));
log.verbose('renew', utils.prettyStringify(response.data))
if (response && response.data && response.data.access_token && response.data.refresh_token) {
result.jwt = response.data.access_token;
result.refreshToken = response.data.refresh_token;
result.jwt = response.data.access_token
result.refreshToken = response.data.refresh_token
} else {
log.error('Access token or refresh token not retreived properly');
log.error('Access token or refresh token not retreived properly')
}
} catch (error) {
log.error('renew', error.message);
log.error('renew', error);
result = error.response && error.response.data;
log.error('renew', error.message)
log.error('renew', error)
result = error.response && error.response.data
}

return result;
return result
},

// Update or remove token based on JWT and user state
async refreshJWT(req, _res, next) {
try {
if (!!req && !!req.user && !!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');
log.verbose('refreshJWT', 'Can refresh JWT token')

// Get new JWT and Refresh Tokens and update the request
let isIdir = (req.session?.passport?.user?._json?.idir_user_guid) ? true : false;
const result = await auth.renew(req.user.refreshToken, isIdir);
req.user.jwt = result.jwt; // eslint-disable-line require-atomic-updates
req.user.refreshToken = result.refreshToken; // eslint-disable-line require-atomic-updates
let isIdir = req.session?.passport?.user?._json?.idir_user_guid ? true : false
const result = await auth.renew(req.user.refreshToken, isIdir)
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');
delete req.user;
log.verbose('refreshJWT', 'Cannot refresh JWT token')
delete req.user
}
}
} else {
log.verbose('refreshJWT', 'No existing User or JWT');
delete req.user;
log.verbose('refreshJWT', 'No existing User or JWT')
delete req.user
}
} catch (error) {
log.error('refreshJWT', error.message);
log.error('refreshJWT', error.message)
}
next();
next()
},

generateUiToken() {
const i = config.get('tokenGenerate:issuer');
const s = '[email protected]';
const a = config.get('server:frontend');
const i = config.get('tokenGenerate:issuer')
const s = '[email protected]'
const a = config.get('server:frontend')
const signOptions = {
issuer: i,
subject: s,
audience: a,
expiresIn: '30m',
algorithm: 'RS256'
};
algorithm: 'RS256',
}

const privateKey = config.get('tokenGenerate:privateKey');
const uiToken = jsonwebtoken.sign({}, privateKey, signOptions);
log.verbose('Generated JWT', uiToken);
return uiToken;
const privateKey = config.get('tokenGenerate:privateKey')
const uiToken = jsonwebtoken.sign({}, privateKey, signOptions)
log.verbose('Generated JWT', uiToken)
return uiToken
},

async getApiCredentials() {
try {
const discovery = await utils.getOidcDiscovery();
const response = await axios.post(discovery.token_endpoint,
const discovery = await utils.getOidcDiscovery()
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
}), {
scope: discovery.scopes_supported,
}),
{
headers: {
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;
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)
}
},
isValidBackendToken() {
return function (req, res, next) {
if (req?.session?.passport?.user?.jwt) {
try {
jsonwebtoken.verify(req.session.passport.user.jwt, config.get('oidc:publicKey'));
jsonwebtoken.verify(req.session.passport.user.jwt, config.get('oidc:publicKey'))
} catch (e) {
log.info('error is from verify', e);
return res.status(HttpStatus.UNAUTHORIZED).json();
log.info('error is from verify', e)
return res.status(HttpStatus.UNAUTHORIZED).json()
}
log.info('Backend token is valid moving to next');
return next();
log.info('Backend token is valid moving to next')
return next()
} else {
log.info('no jwt responding back 401');
return res.status(HttpStatus.UNAUTHORIZED).json();
log.info('no jwt responding back 401')
return res.status(HttpStatus.UNAUTHORIZED).json()
}
};
}
};

}
},
}

module.exports = auth;
module.exports = auth
49 changes: 39 additions & 10 deletions backend/src/components/user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict'
const { getSessionUser, getUserName, getHttpHeader, minify, getUserGuid, isIdirUser } = require('./utils')
const { getSessionUser, getUserName, getBusinessName, getHttpHeader, minify, getUserGuid, isIdirUser, getOperation } = require('./utils')
const config = require('../config/index')
const ApiError = require('./error')
const axios = require('axios')
Expand All @@ -18,12 +18,13 @@ async function getUserInfo(req, res) {
message: 'No session data',
})
}
const userGuid = getUserGuid(req)
const isIdir = isIdirUser(req)
const queryUserName = req.params?.queryUserName
const userName = getUserName(req)
const businessName = getBusinessName(req)

// if is idir user (ministry user), make sure they are a user in dynamics
// TODO commented out until we focus on IDIR login and weather this code is relevant
if (isIdir) {
let response = await getDynamicsUserByEmail(req)
if (response.value?.length > 0 && response.value[0].systemuserid) {
Expand All @@ -35,12 +36,13 @@ async function getUserInfo(req, res) {
})
}
}

let resData = {
// TODO i thing this has to do with impersonate... displayName: (queryUserName)? userName + '-' + queryUserName : userName,
displayName: queryUserName ? userName + '-' + queryUserName : userName,
userName: userName,
businessName: businessName,
isMinistryUser: isIdir,
serverTime: new Date(),
//TODO: unreadMessages is hardcoded. Remove this with API values when built out!
unreadMessages: false,
}
let userResponse = undefined
Expand All @@ -54,19 +56,25 @@ async function getUserInfo(req, res) {
if (userResponse === null) {
return res.status(HttpStatus.NOT_FOUND).json({ message: 'No user found with that BCeID UserName' })
}
userResponse.isImpersonated = true
} catch (e) {
log.error('getUserProfile Error', e.response ? e.response.status : e.message)
throw new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, { message: 'API Get error' }, e)
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(resData)
}
} else {
//If not looking for a username, return from here since ministry staff should not have an account
// TODO: Get info for ministry user from session header for now... until we get an endpiont for ministry
// user from dynamics
resData.userId = userGuid
resData.firstName = req.session.passport.user._json.given_name
resData.lastName = req.session.passport.user._json.family_name
resData.email = req.session.passport.user._json.email
log.verbose('Ministry User response:', resData)
return res.status(HttpStatus.OK).json(resData)
}
} else {
//Not an idir user, so just get the guid from the header
const userGuid = getUserGuid(req)
log.verbose('User Guid is: ', userGuid)
userResponse = await getUserProfile(userGuid)
userResponse = await getUserProfile(userGuid, null)
}

if (log.isVerboseEnabled) {
Expand All @@ -91,12 +99,33 @@ async function getUserInfo(req, res) {
return res.status(HttpStatus.OK).json(results)
}

async function getUserProfile(userGuid) {
async function getDynamicsUserByEmail(req) {
let email = req.session.passport.user._json.email
if (!email) {
//If for some reason, an email is not associated with the IDIR, just use [email protected]
email = `${req.session.passport.user._json.idir_username}@gov.bc.ca`
}
// eslint-disable-next-line quotes,
email.includes("'") ? (email = email.replace("'", "''")) : email
try {
let response = await getOperation(`systemusers?$select=firstname,domainname,lastname&$filter=internalemailaddress eq '${email}'`)
return response
} catch (e) {
log.error('getDynamicsUserByEmail Error', e.response ? e.response.status : e.message)
throw new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, { message: 'API Get error' }, e)
}
}

async function getUserProfile(userGuid, userName) {
try {
let url = undefined

if (userGuid) {
url = config.get('dynamicsApi:apiEndpoint') + `/api/ProviderProfile?userId=${userGuid}`
url = config.get('dynamicsApi:apiEndpoint') + `/api/ProviderProfile?userId=${userGuid}&userName=${userName}`
} else {
url = config.get('dynamicsApi:apiEndpoint') + `/api/ProviderProfile?userName=${userName}`
}

log.verbose('UserProfile Url is', url)
let response = undefined
response = await axios.get(url, getHttpHeader())
Expand Down
Loading

0 comments on commit e660aa5

Please sign in to comment.