Skip to content

Commit

Permalink
Merge pull request #85 from ELEVATE-Project/update/authentication-cha…
Browse files Browse the repository at this point in the history
…nges

authentication changes to handle keycloak token
  • Loading branch information
VISHNUDAS-tunerlabs authored Dec 24, 2024
2 parents 2c600a7 + 06113ca commit 8bfdbc0
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 8 deletions.
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

0 comments on commit 8bfdbc0

Please sign in to comment.