-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
33 changed files
with
1,183 additions
and
375 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
|
@@ -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) { | ||
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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()) | ||
|
Oops, something went wrong.