diff --git a/package-lock.json b/package-lock.json index a5924b0f..e6f0117c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rules-templates", - "version": "0.19.1", + "version": "0.20.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d11de26e..18e9d018 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rules-templates", - "version": "0.19.1", + "version": "0.20.0", "description": "Auth0 Rules Repository", "main": "./rules", "scripts": { diff --git a/rules.json b/rules.json index 1310484b..891bed85 100644 --- a/rules.json +++ b/rules.json @@ -581,6 +581,16 @@ "description": "

Please see the Scaled Access integration for more information and detailed installation instructions.

\n

Required configuration (this Rule will be skipped if any of the below are not defined):

\n\n

Optional configuration:

\n", "code": "function scaledAccessAddRelationshipsClaim(user, context, callback) {\n if (\n !configuration.SCALED_ACCESS_AUDIENCE ||\n !configuration.SCALED_ACCESS_CLIENTID ||\n !configuration.SCALED_ACCESS_CLIENTSECRET ||\n !configuration.SCALED_ACCESS_BASEURL ||\n !configuration.SCALED_ACCESS_TENANT\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const fetch = require('node-fetch');\n const { URLSearchParams } = require('url');\n\n const getM2mToken = () => {\n if (\n global.scaledAccessM2mToken &&\n global.scaledAccessM2mTokenExpiryInMillis > new Date().getTime() + 60000\n ) {\n return Promise.resolve(global.scaledAccessM2mToken);\n } else {\n const tokenUrl = `https://${context.request.hostname}/oauth/token`;\n return fetch(tokenUrl, {\n method: 'POST',\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: configuration.SCALED_ACCESS_CLIENTID,\n client_secret: configuration.SCALED_ACCESS_CLIENTSECRET,\n audience: configuration.SCALED_ACCESS_AUDIENCE,\n scope: 'pg:tenant:admin'\n })\n })\n .then((response) => {\n if (!response.ok) {\n return response.text().then((error) => {\n console.error('Failed to obtain m2m token from ' + tokenUrl);\n throw Error(error);\n });\n } else {\n return response.json();\n }\n })\n .then(({ access_token, expires_in }) => {\n global.scaledAccessM2mToken = access_token;\n global.scaledAccessM2mTokenExpiryInMillis =\n new Date().getTime() + expires_in * 1000;\n return access_token;\n });\n }\n };\n\n const callRelationshipManagementApi = async (accessToken, path) => {\n const url = `${configuration.SCALED_ACCESS_BASEURL}/${configuration.SCALED_ACCESS_TENANT}/${path}`;\n return fetch(url, {\n method: 'GET',\n headers: {\n Authorization: 'Bearer ' + accessToken,\n 'Content-Type': 'application/json'\n }\n }).then(async (response) => {\n if (response.status === 404) {\n return [];\n } else if (!response.ok) {\n return response.text().then((error) => {\n console.error('Failed to call relationship management API', url);\n throw Error(error);\n });\n } else {\n return response.json();\n }\n });\n };\n\n const getRelationships = (accessToken) => {\n return callRelationshipManagementApi(\n accessToken,\n `actors/user/${user.user_id}/relationships`\n );\n };\n\n const addClaimToToken = (apiResponse) => {\n const claimName =\n configuration.SCALED_ACCESS_CUSTOMCLAIM ||\n `https://scaledaccess.com/relationships`;\n context.accessToken[claimName] = apiResponse.map((relationship) => ({\n relationshipType: relationship.relationshipType,\n to: relationship.to\n }));\n };\n\n getM2mToken()\n .then(getRelationships)\n .then(addClaimToToken)\n .then(() => {\n callback(null, user, context);\n })\n .catch((err) => {\n console.error(err);\n console.log('Using configuration: ', JSON.stringify(configuration));\n callback(null, user, context); // fail gracefully, token just won't have extra claim\n });\n}" }, + { + "id": "seczetta-get-risk-score", + "title": "SecZetta Get Risk Score", + "overview": "Grab the risk score from SecZetta to use in the authentication flow", + "categories": [ + "marketplace" + ], + "description": "

Required configuration (this Rule will be skipped if any of the below are not defined):

\n\n

Optional configuration:

\n\n

Helpful Hints

\n", + "code": "async function seczettaGrabRiskScore(user, context, callback) {\n if (\n !configuration.SECZETTA_API_KEY ||\n !configuration.SECZETTA_BASE_URL ||\n !configuration.SECZETTA_ATTRIBUTE_ID ||\n !configuration.SECZETTA_PROFILE_TYPE_ID ||\n !configuration.SECZETTA_ALLOWABLE_RISK ||\n !configuration.SECZETTA_MAXIMUM_ALLOWED_RISK\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const axios = require('axios@0.21.1');\n const URL = require('url').URL;\n\n let profileResponse;\n let riskScoreResponse;\n\n const attributeId = configuration.SECZETTA_ATTRIBUTE_ID;\n const profileTypeId = configuration.SECZETTA_PROFILE_TYPE_ID;\n const allowAuthOnError =\n configuration.SECZETTA_AUTHENTICATE_ON_ERROR === 'true';\n\n // Depends on the configuration\n const uid = user.username || user.email;\n\n const profileRequestUrl = new URL(\n '/api/advanced_search/run',\n configuration.SECZETTA_BASE_URL\n );\n\n const advancedSearchBody = {\n advanced_search: {\n label: 'All Contractors',\n condition_rules_attributes: [\n {\n type: 'ProfileTypeRule',\n comparison_operator: '==',\n value: profileTypeId\n },\n {\n type: 'ProfileAttributeRule',\n condition_object_id: attributeId,\n object_type: 'NeAttribute',\n comparison_operator: '==',\n value: uid\n }\n ]\n }\n };\n\n try {\n profileResponse = await axios.post(\n profileRequestUrl.href,\n advancedSearchBody,\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Token token=' + configuration.SECZETTA_API_KEY,\n Accept: 'application/json'\n }\n }\n );\n\n // If the user is not found via the advanced search\n if (profileResponse.data.profiles.length === 0) {\n console.log('Profile not found. Empty Array sent back!');\n if (allowAuthOnError) {\n return callback(null, user, context);\n }\n return callback(\n new UnauthorizedError('Error retrieving SecZetta Risk Score.')\n );\n }\n } catch (profileError) {\n console.log(\n `Error while calling SecZetta Profile API: ${profileError.message}`\n );\n\n if (allowAuthOnError) {\n return callback(null, user, context);\n }\n\n return callback(\n new UnauthorizedError('Error retrieving SecZetta Risk Score.')\n );\n }\n\n // Should now have the profile in profileResponse. Lets grab it.\n const objectId = profileResponse.data.profiles[0].id;\n\n const riskScoreRequestUrl = new URL(\n '/api/risk_scores?object_id=' + objectId,\n configuration.SECZETTA_BASE_URL\n );\n\n try {\n riskScoreResponse = await axios.get(riskScoreRequestUrl.href, {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Token token=' + configuration.SECZETTA_API_KEY,\n Accept: 'application/json'\n }\n });\n } catch (riskError) {\n console.log(\n `Error while calling SecZetta Risk Score API: ${riskError.message}`\n );\n\n if (allowAuthOnError) {\n return callback(null, user, context);\n }\n\n return callback(\n new UnauthorizedError('Error retrieving SecZetta Risk Score.')\n );\n }\n\n // Should now finally have the risk score. Lets add it to the user\n const riskScoreObj = riskScoreResponse.data.risk_scores[0];\n const overallScore = riskScoreObj.overall_score;\n\n const allowableRisk = parseInt(configuration.SECZETTA_ALLOWABLE_RISK, 10);\n const maximumRisk = parseInt(configuration.SECZETTA_MAXIMUM_ALLOWED_RISK, 10);\n\n // If risk score is below the maxium risk score but above allowable risk: Require MFA\n if (\n (allowableRisk &&\n overallScore > allowableRisk &&\n overallScore < maximumRisk) ||\n allowableRisk === 0\n ) {\n console.log(\n `Risk score ${overallScore} is greater than maximum of ${allowableRisk}. Prompting for MFA.`\n );\n context.multifactor = {\n provider: 'any',\n allowRememberBrowser: false\n };\n return callback(null, user, context);\n }\n\n // If risk score is above the maxium risk score: Fail authN\n if (maximumRisk && overallScore >= maximumRisk) {\n console.log(\n `Risk score ${overallScore} is greater than maximum of ${maximumRisk}`\n );\n return callback(\n new UnauthorizedError(\n `A ${overallScore} risk score is too high. Maximum acceptable risk is ${maximumRisk}.`\n )\n );\n }\n\n if (configuration.SECZETTA_RISK_KEY) {\n context.idToken[configuration.SECZETTA_RISK_KEY] = overallScore;\n context.accessToken[configuration.SECZETTA_RISK_KEY] = overallScore;\n }\n\n return callback(null, user, context);\n}" + }, { "id": "vouched-verification", "title": "Vouched Verification", diff --git a/src/rules/seczetta-get-risk-score.js b/src/rules/seczetta-get-risk-score.js new file mode 100644 index 00000000..ac57ac2e --- /dev/null +++ b/src/rules/seczetta-get-risk-score.js @@ -0,0 +1,186 @@ +/** + * @title SecZetta Get Risk Score + * @overview Grab the risk score from SecZetta to use in the authentication flow + * @gallery true + * @category marketplace + * + * **Required configuration** (this Rule will be skipped if any of the below are not defined): + * + * - `SECZETTA_API_KEY` API Token from your SecZetta tennant + * - `SECZETTA_BASE_URL` URL for your SecZetta tennant + * - `SECZETTA_ATTRIBUTE_ID` the id of the SecZetta attribute you are searching on (i.e personal_email, user_name, etc.) + * - `SECZETTA_PROFILE_TYPE_ID' the id of the profile type this user's profile + * - `SECZETTA_ALLOWABLE_RISK` Set to a risk score integer value above which MFA is required + * - `SECZETTA_MAXIMUM_ALLOWED_RISK` Set to a maximum risk score integer value above which login fails. + * + * **Optional configuration:** + * + * - `SECZETTA_AUTHENTICATE_ON_ERROR` Choose whether or not the rule continues to authenticate on error + * - `SECZETTA_RISK_KEY` The attribute name on the account where the users risk score is stored + * + * **Helpful Hints** + * + * - The SecZetta API documentation is located here: https://{{SECZETTA_BASE_URL}}/api/v1/ + */ +async function seczettaGrabRiskScore(user, context, callback) { + if ( + !configuration.SECZETTA_API_KEY || + !configuration.SECZETTA_BASE_URL || + !configuration.SECZETTA_ATTRIBUTE_ID || + !configuration.SECZETTA_PROFILE_TYPE_ID || + !configuration.SECZETTA_ALLOWABLE_RISK || + !configuration.SECZETTA_MAXIMUM_ALLOWED_RISK + ) { + console.log('Missing required configuration. Skipping.'); + return callback(null, user, context); + } + + const axios = require('axios@0.21.1'); + const URL = require('url').URL; + + let profileResponse; + let riskScoreResponse; + + const attributeId = configuration.SECZETTA_ATTRIBUTE_ID; + const profileTypeId = configuration.SECZETTA_PROFILE_TYPE_ID; + const allowAuthOnError = + configuration.SECZETTA_AUTHENTICATE_ON_ERROR === 'true'; + + // Depends on the configuration + const uid = user.username || user.email; + + const profileRequestUrl = new URL( + '/api/advanced_search/run', + configuration.SECZETTA_BASE_URL + ); + + const advancedSearchBody = { + advanced_search: { + label: 'All Contractors', + condition_rules_attributes: [ + { + type: 'ProfileTypeRule', + comparison_operator: '==', + value: profileTypeId + }, + { + type: 'ProfileAttributeRule', + condition_object_id: attributeId, + object_type: 'NeAttribute', + comparison_operator: '==', + value: uid + } + ] + } + }; + + try { + profileResponse = await axios.post( + profileRequestUrl.href, + advancedSearchBody, + { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Token token=' + configuration.SECZETTA_API_KEY, + Accept: 'application/json' + } + } + ); + + // If the user is not found via the advanced search + if (profileResponse.data.profiles.length === 0) { + console.log('Profile not found. Empty Array sent back!'); + if (allowAuthOnError) { + return callback(null, user, context); + } + return callback( + new UnauthorizedError('Error retrieving SecZetta Risk Score.') + ); + } + } catch (profileError) { + console.log( + `Error while calling SecZetta Profile API: ${profileError.message}` + ); + + if (allowAuthOnError) { + return callback(null, user, context); + } + + return callback( + new UnauthorizedError('Error retrieving SecZetta Risk Score.') + ); + } + + // Should now have the profile in profileResponse. Lets grab it. + const objectId = profileResponse.data.profiles[0].id; + + const riskScoreRequestUrl = new URL( + '/api/risk_scores?object_id=' + objectId, + configuration.SECZETTA_BASE_URL + ); + + try { + riskScoreResponse = await axios.get(riskScoreRequestUrl.href, { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Token token=' + configuration.SECZETTA_API_KEY, + Accept: 'application/json' + } + }); + } catch (riskError) { + console.log( + `Error while calling SecZetta Risk Score API: ${riskError.message}` + ); + + if (allowAuthOnError) { + return callback(null, user, context); + } + + return callback( + new UnauthorizedError('Error retrieving SecZetta Risk Score.') + ); + } + + // Should now finally have the risk score. Lets add it to the user + const riskScoreObj = riskScoreResponse.data.risk_scores[0]; + const overallScore = riskScoreObj.overall_score; + + const allowableRisk = parseInt(configuration.SECZETTA_ALLOWABLE_RISK, 10); + const maximumRisk = parseInt(configuration.SECZETTA_MAXIMUM_ALLOWED_RISK, 10); + + // If risk score is below the maxium risk score but above allowable risk: Require MFA + if ( + (allowableRisk && + overallScore > allowableRisk && + overallScore < maximumRisk) || + allowableRisk === 0 + ) { + console.log( + `Risk score ${overallScore} is greater than maximum of ${allowableRisk}. Prompting for MFA.` + ); + context.multifactor = { + provider: 'any', + allowRememberBrowser: false + }; + return callback(null, user, context); + } + + // If risk score is above the maxium risk score: Fail authN + if (maximumRisk && overallScore >= maximumRisk) { + console.log( + `Risk score ${overallScore} is greater than maximum of ${maximumRisk}` + ); + return callback( + new UnauthorizedError( + `A ${overallScore} risk score is too high. Maximum acceptable risk is ${maximumRisk}.` + ) + ); + } + + if (configuration.SECZETTA_RISK_KEY) { + context.idToken[configuration.SECZETTA_RISK_KEY] = overallScore; + context.accessToken[configuration.SECZETTA_RISK_KEY] = overallScore; + } + + return callback(null, user, context); +}