Skip to content

Commit

Permalink
Seczetta risk integration (#277)
Browse files Browse the repository at this point in the history
* Added SecZetta risk score integration to rules directory

* Updated to include error checking when a user isnt found in SecZett

* Updates based on Josh's suggestions.

* Updated Axios + Use Node's URL class

* checking for allowable risk === 0

* This is my 3rd test commit. Updated markdown file

* header edits

* npm run format

* remove unnecessary require

* refactor SECZETTA_AUTHENTICATE_ON_ERROR

* better error messages

* better error messages

* remove readme

* variable declaration

* comments and whitespace

* remove logging

* rename rule

* npm version && npm run release

Co-authored-by: Taylor Hook <[email protected]>
Co-authored-by: Josh Cunningham <[email protected]>
Co-authored-by: Josh Cunningham <[email protected]>
  • Loading branch information
4 people authored Apr 2, 2021
1 parent 91e97ad commit 6b5e9b1
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rules-templates",
"version": "0.19.1",
"version": "0.20.0",
"description": "Auth0 Rules Repository",
"main": "./rules",
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,16 @@
"description": "<p>Please see the <a href=\"https://marketplace.auth0.com/integrations/scaled-access\">Scaled Access integration</a> for more information and detailed installation instructions.</p>\n<p><strong>Required configuration</strong> (this Rule will be skipped if any of the below are not defined):</p>\n<ul>\n<li><code>SCALED_ACCESS_AUDIENCE</code> The identifier of the Auth0 API</li>\n<li><code>SCALED_ACCESS_CLIENTID</code> The Client ID of the Auth0 machine-to-machine application.</li>\n<li><code>SCALED_ACCESS_CLIENTSECRET</code> The Client secret of the Auth0 machine-to-machine application.</li>\n<li><code>SCALED_ACCESS_BASEURL</code> The base URL for the Relationship Management API.</li>\n<li><code>SCALED_ACCESS_TENANT</code> Your tenant code provided by Scaled Access.</li>\n</ul>\n<p><strong>Optional configuration:</strong></p>\n<ul>\n<li><code>SCALED_ACCESS_CUSTOMCLAIM</code> A namespaced ID token claim (defaults to <code>https://scaledaccess.com/relationships</code>)</li>\n</ul>",
"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": "<p><strong>Required configuration</strong> (this Rule will be skipped if any of the below are not defined):</p>\n<ul>\n<li><code>SECZETTA_API_KEY</code> API Token from your SecZetta tennant</li>\n<li><code>SECZETTA_BASE_URL</code> URL for your SecZetta tennant</li>\n<li><code>SECZETTA_ATTRIBUTE_ID</code> the id of the SecZetta attribute you are searching on (i.e personal<em>email, user</em>name, etc.)</li>\n<li>`SECZETTA<em>PROFILE</em>TYPE_ID' the id of the profile type this user's profile</li>\n<li><code>SECZETTA_ALLOWABLE_RISK</code> Set to a risk score integer value above which MFA is required</li>\n<li><code>SECZETTA_MAXIMUM_ALLOWED_RISK</code> Set to a maximum risk score integer value above which login fails.</li>\n</ul>\n<p><strong>Optional configuration:</strong></p>\n<ul>\n<li><code>SECZETTA_AUTHENTICATE_ON_ERROR</code> Choose whether or not the rule continues to authenticate on error</li>\n<li><code>SECZETTA_RISK_KEY</code> The attribute name on the account where the users risk score is stored</li>\n</ul>\n<p><strong>Helpful Hints</strong></p>\n<ul>\n<li>The SecZetta API documentation is located here: https://{{SECZETTA<em>BASE</em>URL}}/api/v1/</li>\n</ul>",
"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('[email protected]');\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",
Expand Down
186 changes: 186 additions & 0 deletions src/rules/seczetta-get-risk-score.js
Original file line number Diff line number Diff line change
@@ -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('[email protected]');
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);
}

0 comments on commit 6b5e9b1

Please sign in to comment.