Skip to content
Merged
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: 2 additions & 0 deletions src/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ API_DOC_URL = "/entity-management/api-doc"
#Indicate If auth token is bearer or not
IS_AUTH_TOKEN_BEARER=false

AUTH_METHOD = native #or keycloak_public_key
KEYCLOAK_PUBLIC_KEY_PATH = path to the pem/secret file
10 changes: 10 additions & 0 deletions src/envVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ let enviromentVariables = {
optional: true,
default: false,
},
AUTH_METHOD: {
message: 'Required authentication method',
optional: true,
default: CONSTANTS.common.AUTH_METHOD.NATIVE,
},
KEYCLOAK_PUBLIC_KEY_PATH: {
message: 'Required Keycloak Public Key Path',
optional: true,
default: '../keycloakPublicKeys',
},
}

let success = true
Expand Down
4 changes: 4 additions & 0 deletions src/generics/constants/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ module.exports = {
GET_METHOD: 'GET',
ENTITYTYPE: 'entityType',
GROUPS: 'groups',
AUTH_METHOD: {
NATIVE: 'native',
KEYCLOAK_PUBLIC_KEY: 'keycloak_public_key',
},
}
2 changes: 2 additions & 0 deletions src/generics/keycloakPublicKeys/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
87 changes: 79 additions & 8 deletions src/generics/middleware/authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// dependencies
const jwt = require('jsonwebtoken')
const isBearerRequired = process.env.IS_AUTH_TOKEN_BEARER === 'true'
const path = require('path')
const fs = require('fs')
var respUtil = function (resp) {
return {
status: resp.errCode,
Expand Down Expand Up @@ -86,29 +88,98 @@ module.exports = async function (req, res, next, token = '') {
token = token?.trim()
}

rspObj.errCode = CONSTANTS.apiResponses.TOKEN_INVALID_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_INVALID_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status

// <---- For Elevate user service user compactibility ---->
let decodedToken = null
try {
decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.NATIVE) {
try {
// If using native authentication, verify the JWT using the secret key
decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
} catch (err) {
// If verification fails, send an unauthorized response
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
} else if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.KEYCLOAK_PUBLIC_KEY) {
// If using Keycloak with a public key for authentication
const keycloakPublicKeyPath = `${process.env.KEYCLOAK_PUBLIC_KEY_PATH}/`
const PEM_FILE_BEGIN_STRING = '-----BEGIN PUBLIC KEY-----'
const PEM_FILE_END_STRING = '-----END PUBLIC KEY-----'

// Decode the JWT to extract its claims without verifying
const tokenClaims = jwt.decode(token, { complete: true })

if (!tokenClaims || !tokenClaims.header) {
// If the token does not contain valid claims or header, send an unauthorized response
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}

// Extract the key ID (kid) from the token header
const kid = tokenClaims.header.kid

// Construct the path to the public key file using the key ID
let filePath = path.resolve(__dirname, keycloakPublicKeyPath, kid.replace(/\.\.\//g, ''))

// Read the public key file from the resolved file path
const accessKeyFile = await fs.promises.readFile(filePath, 'utf8')

// Ensure the public key is properly formatted with BEGIN and END markers
const cert = accessKeyFile.includes(PEM_FILE_BEGIN_STRING)
? accessKeyFile
: `${PEM_FILE_BEGIN_STRING}\n${accessKeyFile}\n${PEM_FILE_END_STRING}`
let verifiedClaims
try {
// Verify the JWT using the public key and specified algorithms
verifiedClaims = jwt.verify(token, cert, { algorithms: ['sha1', 'RS256', 'HS256'] })
} catch (err) {
// If the token is expired or any other error occurs during verification
if (err.name === 'TokenExpiredError') {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_INVALID_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_INVALID_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
}

// Extract the external user ID from the verified claims
const externalUserId = verifiedClaims.sub.split(':').pop()

const data = {
id: externalUserId,
roles: [], // this is temporariy set to an empty array, it will be corrected soon...
name: verifiedClaims.name,
organization_id: verifiedClaims.org || null,
}

// Ensure decodedToken is initialized as an object
decodedToken = decodedToken || {}
decodedToken['data'] = data
}
} catch (err) {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}
if (!decodedToken) {
rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE
rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE
rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status
return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj))
}

req.userDetails = {
userToken: token,
userInformation: {
userId: decodedToken.data.id.toString(),
userId: typeof decodedToken.data.id == 'string' ? decodedToken.data.id : decodedToken.data.id.toString(),
userName: decodedToken.data.name,
// email : decodedToken.data.email, //email is removed from token
firstName: decodedToken.data.name,
roles: decodedToken.data.roles.map((role) => role.title),
entityTypes: 'state',
},
}
next()
Expand Down