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.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nSCALED_ACCESS_AUDIENCE
The identifier of the Auth0 API \nSCALED_ACCESS_CLIENTID
The Client ID of the Auth0 machine-to-machine application. \nSCALED_ACCESS_CLIENTSECRET
The Client secret of the Auth0 machine-to-machine application. \nSCALED_ACCESS_BASEURL
The base URL for the Relationship Management API. \nSCALED_ACCESS_TENANT
Your tenant code provided by Scaled Access. \n
\nOptional configuration:
\n\nSCALED_ACCESS_CUSTOMCLAIM
A namespaced ID token claim (defaults to https://scaledaccess.com/relationships
) \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\nSECZETTA_API_KEY
API Token from your SecZetta tennant \nSECZETTA_BASE_URL
URL for your SecZetta tennant \nSECZETTA_ATTRIBUTE_ID
the id of the SecZetta attribute you are searching on (i.e personalemail, username, etc.) \n- `SECZETTAPROFILETYPE_ID' the id of the profile type this user's profile
\nSECZETTA_ALLOWABLE_RISK
Set to a risk score integer value above which MFA is required \nSECZETTA_MAXIMUM_ALLOWED_RISK
Set to a maximum risk score integer value above which login fails. \n
\nOptional configuration:
\n\nSECZETTA_AUTHENTICATE_ON_ERROR
Choose whether or not the rule continues to authenticate on error \nSECZETTA_RISK_KEY
The attribute name on the account where the users risk score is stored \n
\nHelpful Hints
\n\n- The SecZetta API documentation is located here: https://{{SECZETTABASEURL}}/api/v1/
\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);
+}