diff --git a/package-lock.json b/package-lock.json
index bce795e5..f16ae69b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rules-templates",
- "version": "0.23.0",
+ "version": "0.24.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rules-templates",
- "version": "0.23.0",
+ "version": "0.24.0",
"license": "ISC",
"devDependencies": {
"ajv": "^6.5.4",
diff --git a/package.json b/package.json
index 07de4502..8059f04c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rules-templates",
- "version": "0.23.0",
+ "version": "0.24.0",
"description": "Auth0 Rules Repository",
"main": "./rules",
"scripts": {
diff --git a/rules.json b/rules.json
index b5a383db..a5d1be35 100644
--- a/rules.json
+++ b/rules.json
@@ -282,7 +282,7 @@
"categories": [
"enrich profile"
],
- "description": "
This rule will add a country
attribute to the user based on their ip address.
\nExample geoip object:
\n\"geoip\": {\n \"country_code\": \"AR\",\n \"country_code3\": \"ARG\",\n \"country_name\": \"Argentina\",\n \"region\": \"05\",\n \"city\": \"Cordoba\",\n \"latitude\": -31.41349983215332,\n \"longitude\": -64.18109893798828,\n \"continent_code\": \"SA\",\n \"time_zone\": \"America/Argentina/Cordoba\"\n}\n
",
+ "description": "This rule will add a country
attribute to the user based on their ip address.
\nExample geoip object:
\n\n\"geoip\": {\n \"country_code\": \"AR\",\n \"country_code3\": \"ARG\",\n \"country_name\": \"Argentina\",\n \"region\": \"05\",\n \"city\": \"Cordoba\",\n \"latitude\": -31.41349983215332,\n \"longitude\": -64.18109893798828,\n \"continent_code\": \"SA\",\n \"time_zone\": \"America/Argentina/Cordoba\"\n}\n
",
"code": "function addCountry(user, context, callback) {\n if (context.request.geoip) {\n context.idToken['https://example.com/country'] =\n context.request.geoip.country_name;\n context.idToken['https://example.com/timezone'] =\n context.request.geoip.time_zone;\n }\n\n callback(null, user, context);\n}"
},
{
@@ -383,7 +383,7 @@
"enrich profile"
],
"description": "This rule will set the picture
to the original sized profile picture for users who login with LinkedIn.
",
- "code": "function useOriginallinkedInProfilePicture(user, context, callback) {\n if (context.connection !== 'linkedin') {\n return callback(null, user, context);\n }\n\n const _ = require('lodash');\n const request = require('request');\n\n const liIdentity = _.find(user.identities, { connection: 'linkedin' });\n\n const options = {\n url:\n 'https://api.linkedin.com/v1/people/~/picture-urls::(original)?format=json',\n headers: {\n Authorization: 'Bearer ' + liIdentity.access_token\n },\n json: true\n };\n\n request(options, function (error, response, body) {\n if (error) return callback(error);\n if (response.statusCode !== 200) return callback(new Error(body));\n\n if (body.values && body.values.length >= 1) {\n context.idToken.picture = body.values[0];\n }\n\n return callback(null, user, context);\n });\n}"
+ "code": "function useOriginallinkedInProfilePicture(user, context, callback) {\n if (context.connection !== 'linkedin') {\n return callback(null, user, context);\n }\n\n const _ = require('lodash');\n const request = require('request');\n\n const liIdentity = _.find(user.identities, { connection: 'linkedin' });\n\n const options = {\n url: 'https://api.linkedin.com/v1/people/~/picture-urls::(original)?format=json',\n headers: {\n Authorization: 'Bearer ' + liIdentity.access_token\n },\n json: true\n };\n\n request(options, function (error, response, body) {\n if (error) return callback(error);\n if (response.statusCode !== 200) return callback(new Error(body));\n\n if (body.values && body.values.length >= 1) {\n context.idToken.picture = body.values[0];\n }\n\n return callback(null, user, context);\n });\n}"
},
{
"id": "migrate-root-attributes",
@@ -434,7 +434,7 @@
"enrich profile"
],
"description": "This rule shows how to query a basic profile http binding SOAP web service for roles and add those to the user.
",
- "code": "function getRolesFromSoapService(user, context, callback) {\n const request = require('request');\n const xmldom = require('xmldom');\n const xpath = require('xpath');\n\n function getRoles(cb) {\n request.post(\n {\n url: 'https://somedomain.com/RoleService.svc',\n body:\n '',\n headers: {\n 'Content-Type': 'text/xml; charset=utf-8',\n SOAPAction: 'http://tempuri.org/RoleService/GetRolesForCurrentUser'\n }\n },\n function (err, response, body) {\n if (err) return cb(err);\n\n const parser = new xmldom.DOMParser();\n const doc = parser.parseFromString(body);\n const roles = xpath\n .select(\"//*[local-name(.)='string']\", doc)\n .map(function (node) {\n return node.textContent;\n });\n return cb(null, roles);\n }\n );\n }\n\n getRoles(function (err, roles) {\n if (err) return callback(err);\n\n context.idToken['https://example.com/roles'] = roles;\n\n callback(null, user, context);\n });\n}"
+ "code": "function getRolesFromSoapService(user, context, callback) {\n const request = require('request');\n const xmldom = require('xmldom');\n const xpath = require('xpath');\n\n function getRoles(cb) {\n request.post(\n {\n url: 'https://somedomain.com/RoleService.svc',\n body: '',\n headers: {\n 'Content-Type': 'text/xml; charset=utf-8',\n SOAPAction: 'http://tempuri.org/RoleService/GetRolesForCurrentUser'\n }\n },\n function (err, response, body) {\n if (err) return cb(err);\n\n const parser = new xmldom.DOMParser();\n const doc = parser.parseFromString(body);\n const roles = xpath\n .select(\"//*[local-name(.)='string']\", doc)\n .map(function (node) {\n return node.textContent;\n });\n return cb(null, roles);\n }\n );\n }\n\n getRoles(function (err, roles) {\n if (err) return callback(err);\n\n context.idToken['https://example.com/roles'] = roles;\n\n callback(null, user, context);\n });\n}"
},
{
"id": "socure-fraudscore",
@@ -468,161 +468,6 @@
}
]
},
- {
- "name": "marketplace",
- "templates": [
- {
- "id": "arengu-policy-acceptance",
- "title": "Arengu Policy Acceptance",
- "overview": "Require your users to accept custom privacy policies or new terms.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Aregnu Policy Acceptance integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nSESSION_TOKEN_SECRET
: A long, random string at least 32 bytes long \nARENGU_POLICIES_FORM_URL
: The URL that contains an embedded form or with a hosted form page \n
",
- "code": "async function arenguCheckUserPolicies(user, context, callback) {\n if (\n !configuration.SESSION_TOKEN_SECRET ||\n !configuration.ARENGU_POLICIES_FORM_URL\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const {\n Auth0RedirectRuleUtilities,\n Auth0UserUpdateUtilities\n } = require('@auth0/rule-utilities@0.2.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const userUtils = new Auth0UserUpdateUtilities(user, auth0);\n\n function mustAcceptNewPolicies() {\n // Use .getAppMeta() or .getUserMeta() as appropriate and modify the property key and value to your needs.\n return userUtils.getAppMeta('terms_accepted') !== true;\n }\n\n function validateSessionToken() {\n try {\n return ruleUtils.validateSessionToken();\n } catch (error) {\n callback(error);\n }\n }\n\n if (ruleUtils.isRedirectCallback && ruleUtils.queryParams.session_token) {\n const decodedToken = validateSessionToken();\n const customClaims = decodedToken.other;\n\n for (const [key, value] of Object.entries(customClaims)) {\n // Use .setAppMeta() or .setUserMeta() as appropriate\n userUtils.setAppMeta(key, value);\n }\n\n try {\n // Use .updateAppMeta() or .updateUserMeta() as appropriate\n await userUtils.updateAppMeta();\n\n return callback(null, user, context);\n } catch (error) {\n return callback(error);\n }\n }\n\n if (mustAcceptNewPolicies()) {\n ruleUtils.doRedirect(configuration.ARENGU_POLICIES_FORM_URL);\n\n return callback(null, user, context);\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "arengu-progressive-profiling",
- "title": "Arengu Progressive Profiling",
- "overview": "Capture new users' information in your authentication flows.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Aregnu Progressive Profiling integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nSESSION_TOKEN_SECRET
: A long, random string at least 32 bytes long \nARENGU_PROFILE_FORM_URL
: The URL that contains an embedded form or with a hosted form page \n
",
- "code": "async function arenguCompleteUserProfile(user, context, callback) {\n if (\n !configuration.SESSION_TOKEN_SECRET ||\n !configuration.ARENGU_PROFILE_FORM_URL\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const {\n Auth0RedirectRuleUtilities,\n Auth0UserUpdateUtilities\n } = require('@auth0/rule-utilities@0.2.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const userUtils = new Auth0UserUpdateUtilities(user, auth0);\n\n function validateSessionToken() {\n try {\n return ruleUtils.validateSessionToken();\n } catch (error) {\n callback(error);\n }\n }\n\n // Modify your login criteria to your needs\n function isLogin() {\n const loginCount = configuration.ARENGU_PROFILE_LOGIN_COUNT || 2;\n return context.stats.loginsCount > parseInt(loginCount, 10);\n }\n\n function isEmptyUserMeta(key) {\n return (\n userUtils.getUserMeta(key) === undefined ||\n userUtils.getUserMeta(key) === null ||\n userUtils.getUserMeta(key).length === 0\n );\n }\n\n function isProfileIncomplete() {\n // Add your required user_medata keys\n return isEmptyUserMeta('job_title') || isEmptyUserMeta('company_name');\n }\n\n if (ruleUtils.isRedirectCallback && ruleUtils.queryParams.session_token) {\n const decodedToken = validateSessionToken();\n const customClaims = decodedToken.other;\n\n for (const [key, value] of Object.entries(customClaims)) {\n userUtils.setUserMeta(key, value);\n }\n\n try {\n await userUtils.updateUserMeta();\n\n return callback(null, user, context);\n } catch (error) {\n return callback(error);\n }\n }\n\n if (isLogin() && isProfileIncomplete()) {\n ruleUtils.doRedirect(configuration.ARENGU_PROFILE_FORM_URL);\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "cumulio-add-metadata-to-tokens",
- "title": "User metadata for Cumul.io",
- "overview": "Add Cumul.io user metadata to tokens to be used for Cumul.io dashboard filtering",
- "categories": [
- "marketplace"
- ],
- "description": "This integration simplifies the process of making full use of integrated Cumul.io dashboards' multi tenant features\nby using Auth0 as its authentication layer. The integration will allow you to set up and use user\ninformation in Auth0 to filter and structure your Cumul.io dashboards.
",
- "code": "function addMetadataToTokens(user, context, callback) {\n const namespace = 'https://cumulio/';\n user.user_metadata = user.user_metadata || {};\n const cumulioMetadata = user.user_metadata.cumulio || {};\n if (typeof cumulioMetadata === 'object' && cumulioMetadata !== null) {\n Object.keys(cumulioMetadata).forEach((k) => {\n context.idToken[namespace + k] = cumulioMetadata[k];\n context.accessToken[namespace + k] = cumulioMetadata[k];\n });\n } else {\n console.log(\n 'Make sure that user_metadata.cumulio is an object with keys and values'\n );\n return;\n }\n callback(null, user, context);\n}"
- },
- {
- "id": "eva-voice-biometric",
- "title": "EVA Voice Biometric connector",
- "overview": "EVA Voice Biometric connector rule for Auth0 enables voice enrolment and verification as a second factor",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the EVA Voice Biometrics integration for more information and detailed installation instructions.
\nOptional configuration:
\n\nAURAYA_URL
EVA endpoint, typically: https://eva-web.mydomain.com/server/oauth \nAURAYA_CLIENT_ID
JWT client id on the EVA server (and this server) \nAURAYA_CLIENT_SECRET
JWT client secret on the EVA server (and this server) \nAURAYA_ISSUER
This app (or \"issuer\") \nAURAYA_RANDOM_DIGITS
Set to \"true\" to prompt for random digits or \"false\" not to \nAURAYA_COMMON_DIGITS
Set to \"true\" to prompt for common digits or \"false\" not to \nAURAYA_PERSONAL_DIGITS
A user.usermetadata property that contains digits such as phonenumber \nAURAYA_COMMON_DIGITS_PROMPT
A digit string to prompt for common digits (e.g '987654321') \nAURAYA_PERSONAL_DIGITS_PROMPT
A string to prompt for personal digits (e.g 'your cell number') \nAURAYA_DEBUG
Set to \"true\" to log errors in the console \n
",
- "code": "function evaVoiceBiometric(user, context, callback) {\n const debug = typeof configuration.AURAYA_DEBUG !== 'undefined';\n if (debug) {\n console.log(user);\n console.log(context);\n console.log(configuration);\n }\n\n const eva_url =\n configuration.AURAYA_URL ||\n 'https://eval-eva-web.aurayasystems.com/server/oauth';\n const clientSecret =\n configuration.AURAYA_CLIENT_SECRET ||\n 'o4X0LFKi2caP5ipUwaF4B27cZmfOIh0JXnqmfiC4mHkVskSzbp72Emk3AB6';\n const clientId = configuration.AURAYA_CLIENT_ID || 'auraya';\n const issuer = configuration.AURAYA_ISSUER || 'issuer';\n\n // Prepare user's enrolment status\n user.user_metadata = user.user_metadata || {};\n user.user_metadata.auraya_eva = user.user_metadata.auraya_eva || {};\n\n // User has initiated a login and is prompted to use voice biometrics\n // Send user's information and query params in a JWT to avoid tampering\n function createToken(user) {\n const options = {\n expiresInMinutes: 2,\n audience: clientId,\n issuer: issuer\n };\n\n return jwt.sign(user, clientSecret, options);\n }\n\n if (context.protocol === 'redirect-callback') {\n // user was redirected to the /continue endpoint with correct state parameter value\n\n var options = {\n //subject: user.user_id, // validating the subject is nice to have but not strictly necessary\n jwtid: user.jti // unlike state, this value can't be spoofed by DNS hacking or inspecting the payload\n };\n\n const payload = jwt.verify(\n context.request.body.token,\n clientSecret,\n options\n );\n if (debug) {\n console.log(payload);\n }\n\n if (payload.reason === 'enrolment_succeeded') {\n user.user_metadata.auraya_eva.status = 'enrolled';\n\n console.log('Biometric user successfully enrolled');\n // persist the user_metadata update\n auth0.users\n .updateUserMetadata(user.user_id, user.user_metadata)\n .then(function () {\n callback(null, user, context);\n })\n .catch(function (err) {\n callback(err);\n });\n\n return;\n }\n\n if (payload.reason !== 'verification_accepted') {\n // logic to detect repeatedly rejected attempts could go here\n // and update the eva.status accordingly (perhaps with 'blocked')\n console.log(`Biometric rejection reason: ${payload.reason}`);\n return callback(new UnauthorizedError(payload.reason), user, context);\n }\n\n // verification accepted\n console.log('Biometric verification accepted');\n return callback(null, user, context);\n }\n\n const url = require('url@0.10.3');\n user.jti = uuid.v4();\n user.user_metadata.auraya_eva.status =\n user.user_metadata.auraya_eva.status || 'initial';\n const mode =\n user.user_metadata.auraya_eva.status === 'initial' ? 'enrol' : 'verify';\n\n // returns property of the user.user_metadata object, typically \"phone_number\"\n // default is '', (server skips this prompt)\n\n let personalDigits = '';\n if (typeof configuration.AURAYA_PERSONAL_DIGITS !== 'undefined') {\n personalDigits = user.user_metadata[configuration.AURAYA_PERSONAL_DIGITS];\n }\n\n // default value for these is 'true'\n const commonDigits = configuration.AURAYA_COMMON_DIGITS || 'true';\n const randomDigits = configuration.AURAYA_RANDOM_DIGITS || 'true';\n\n // default value for these is '' (the server default)\n const commonDigitsPrompt = configuration.AURAYA_COMMON_DIGITS_PROMPT || ''; // 123456789\n const personalDigitsPrompt =\n configuration.AURAYA_PERSONAL_DIGITS_PROMPT || ''; // 'your phone number'\n\n const token = createToken({\n sub: user.user_id,\n jti: user.jti,\n oauth: {\n state: '', // not used in token, only in the GET request\n callbackURL: url.format({\n protocol: 'https',\n hostname: context.request.hostname,\n pathname: '/continue'\n }),\n nonce: user.jti // performs same function as jti\n },\n biometric: {\n id: user.user_id, // email - can be used for identities that cross IdP boundaries\n mode: mode,\n personalDigits: personalDigits,\n personalDigitsPrompt: personalDigitsPrompt,\n commonDigits: commonDigits,\n commonDigitsPrompt: commonDigitsPrompt,\n randomDigits: randomDigits\n }\n });\n\n context.redirect = {\n url: `${eva_url}?token=${token}`\n };\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "iddataweb-verification-workflow",
- "title": "ID DataWeb Verification Workflow",
- "overview": "Verify your user's identity in 180+ countries with ID DataWeb's adaptive Verification Workflows.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the ID DataWeb integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nIDDATAWEB_BASE_URL
Indicates the ID DataWeb environment. The default value is Pre-production - https://prod2.iddataweb.com/prod-axn
- where all testing and POCs should take place. To switch to production, change the URL to https://prod2.iddataweb.com/prod-axn
\nIDDATAWEB_CLIENT_ID
Identifies your specific verification workflow and user experience. Get this from the ID DataWeb’s AXN Admin console. \nIDDATAWEB_CLIENT_SECRET
Authenticates your specific verification workflow and user experience. Get this from ID DataWeb’s AXN Admin console. \n
\nOptional configuration:
\n\nIDDATAWEB_ALWAYS_VERIFY
Controls if users are verified each time they login, or just initially. We recommend \"true\" (verify the user on every login) for testing, not set (verify once, then not again) for production. \n
",
- "code": "async function iddatawebVerificationWorkflow(user, context, callback) {\n const {\n IDDATAWEB_BASE_URL,\n IDDATAWEB_CLIENT_ID,\n IDDATAWEB_CLIENT_SECRET,\n IDDATAWEB_ALWAYS_VERIFY\n } = configuration;\n\n if (!IDDATAWEB_BASE_URL || !IDDATAWEB_CLIENT_ID || !IDDATAWEB_CLIENT_SECRET) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n const axiosClient = require('axios@0.21.1');\n const url = require('url');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const idwBasicAuth = Buffer.from(\n IDDATAWEB_CLIENT_ID + ':' + IDDATAWEB_CLIENT_SECRET\n ).toString('base64');\n\n const idwTokenNamepsace = 'https://iddataweb.com/';\n const idwTokenEndpoint = `${IDDATAWEB_BASE_URL}/axn/oauth2/token`;\n const idwAuthorizeEndpoint = `${IDDATAWEB_BASE_URL}/axn/oauth2/authorize`;\n const auth0ContinueUrl = `https://${context.request.hostname}/continue`;\n\n let iddataweb = (user.app_metadata && user.app_metadata.iddataweb) || {};\n iddataweb.verificationResult = iddataweb.verificationResult || {};\n\n // if the user is already verified and we don't need to check, exit\n if (\n iddataweb.verificationResult.policyDecision === 'approve' &&\n IDDATAWEB_ALWAYS_VERIFY !== 'true'\n ) {\n console.log('user ' + user.user_id + ' has been previously verified.');\n return callback(null, user, context);\n }\n\n // if coming back from redirect - get token, make policy decision, and update user metadata.\n if (ruleUtils.isRedirectCallback) {\n console.log('code from IDW: ' + ruleUtils.queryParams.code);\n\n const formParams = new url.URLSearchParams({\n grant_type: 'authorization_code',\n code: ruleUtils.queryParams.code,\n redirect_uri: auth0ContinueUrl\n });\n\n const headers = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Cache-Control': 'no-cache',\n Authorization: `Basic ${idwBasicAuth}`\n };\n\n let decodedToken;\n try {\n const tokenResponse = await axiosClient.post(\n idwTokenEndpoint,\n formParams.toString(),\n { headers }\n );\n\n if (tokenResponse.data.error) {\n throw new Error(tokenResponse.data.error_description);\n }\n\n decodedToken = jwt.decode(tokenResponse.data.id_token);\n } catch (error) {\n return callback(error);\n }\n\n //check issuer, audience and experiation of ID DataWeb Token\n if (\n decodedToken.iss !== IDDATAWEB_BASE_URL ||\n decodedToken.aud !== IDDATAWEB_CLIENT_ID\n ) {\n return callback(new Error('ID token invalid.'));\n }\n\n console.log('policy decision: ' + decodedToken.policyDecision);\n console.log('score: ' + decodedToken.idwTrustScore);\n console.log('IDW transaction ID: ' + decodedToken.jti);\n\n // once verification is complete, update user's metadata in Auth0.\n //this could be used for downstream application authorization,\n //or mapping access to levels of assurance.\n iddataweb.verificationResult = {\n policyDecision: decodedToken.policyDecision,\n transactionid: decodedToken.jti,\n iat: decodedToken.iat\n };\n\n try {\n auth0.users.updateAppMetadata(user.user_id, { iddataweb });\n } catch (error) {\n return callback(error);\n }\n\n //include ID DataWeb results in Auth0 ID Token\n context.idToken[idwTokenNamepsace + 'policyDecision'] =\n decodedToken.policyDecision;\n context.idToken[idwTokenNamepsace + 'transactionId'] = decodedToken.jti;\n context.idToken[idwTokenNamepsace + 'iat'] = decodedToken.iat;\n\n return callback(null, user, context);\n }\n\n // ... otherwise, redirect for verification.\n\n let idwRedirectUrl =\n idwAuthorizeEndpoint +\n '?client_id=' +\n IDDATAWEB_CLIENT_ID +\n '&redirect_uri=' +\n auth0ContinueUrl +\n '&scope=openid+country.US&response_type=code';\n\n if (ruleUtils.canRedirect) {\n context.redirect = {\n url: idwRedirectUrl\n };\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "incognia-authentication",
- "title": "Incognia Authentication Rule",
- "overview": "Verify if the device logging in is at a trusted location.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Incognia Authentication integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nINCOGNIA_CLIENT_ID
: The client ID obtained from Incognia's dashboard (My Apps > API Credentials) \nINCOGNIA_CLIENT_SECRET
: The client secret obtained from Incognia's dashboard (My Apps > API Credentials) \n
",
- "code": "async function incogniaAuthenticationRule(user, context, callback) {\n const _ = require('lodash@4.17.19');\n\n const { IncogniaAPI } = require('@incognia/api@1.1.1');\n\n const {\n INCOGNIA_CLIENT_ID,\n INCOGNIA_CLIENT_SECRET,\n INCOGNIA_REGION\n } = configuration;\n\n if (!INCOGNIA_CLIENT_ID || !INCOGNIA_CLIENT_SECRET) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const installationId = _.get(\n context,\n 'request.query.incognia_installation_id'\n );\n if (!installationId) {\n console.log('Missing installation_id. Skipping.');\n return callback(null, user, context);\n }\n\n const accountId = _.get(user, 'user_id');\n if (!accountId) {\n console.log('Missing user_id. Skipping.');\n return callback(null, user, context);\n }\n\n let incogniaAPI;\n if (global.incogniaAPI) {\n incogniaAPI = global.incogniaAPI;\n } else {\n incogniaAPI = new IncogniaAPI({\n clientId: INCOGNIA_CLIENT_ID,\n clientSecret: INCOGNIA_CLIENT_SECRET,\n region: INCOGNIA_REGION\n });\n global.incogniaAPI = incogniaAPI;\n }\n\n try {\n const loginAssessment = await incogniaAPI.registerLoginAssessment({\n installationId: installationId,\n accountId: accountId\n });\n\n // Incognia's risk assessment will be in a namespaced claim so it can be used in other rules\n // for skipping/prompting MFA or in the mobile app itself to decide whether the user should be\n // redirected to step-up auth for example.\n context.idToken['https://www.incognia.com/assessment'] =\n loginAssessment.riskAssessment;\n } catch (error) {\n console.log('Error calling Incognia API for a new login.');\n return callback(error);\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "incognia-onboarding",
- "title": "Incognia Onboarding Rule",
- "overview": "Verify if the device location behavior matches the address declared during onboarding.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Incognia Onboarding integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nINCOGNIA_CLIENT_ID
: The client ID obtained from Incognia's dashboard (My Apps > API Credentials) \nINCOGNIA_CLIENT_SECRET
: The client secret obtained from Incognia's dashboard (My Apps > API Credentials) \n
",
- "code": "async function incogniaOnboardingRule(user, context, callback) {\n const _ = require('lodash@4.17.19');\n\n const { IncogniaAPI } = require('@incognia/api@1.1.1');\n const { Auth0UserUpdateUtilities } = require('@auth0/rule-utilities@0.2.0');\n\n const {\n INCOGNIA_CLIENT_ID,\n INCOGNIA_CLIENT_SECRET,\n INCOGNIA_REGION,\n INCOGNIA_HOME_ADDRESS_PROP\n } = configuration;\n\n if (!INCOGNIA_CLIENT_ID || !INCOGNIA_CLIENT_SECRET) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const installationId = _.get(\n context,\n 'request.query.incognia_installation_id'\n );\n if (!installationId) {\n console.log('Missing installation_id. Skipping.');\n return callback(null, user, context);\n }\n\n // User home address should be set using Auth0's Signup API for example. If the home address is\n // not in 'user_metadata.home_address', please specify the path of the field inside the user\n // object where the home address is through the INCOGNIA_HOME_ADDRESS_PROP configuration.\n const homeAddressProp =\n INCOGNIA_HOME_ADDRESS_PROP || 'user_metadata.home_address';\n const homeAddress = _.get(user, homeAddressProp);\n if (!homeAddress) {\n console.log('Missing user home address. Skipping.');\n return callback(null, user, context);\n }\n\n const userUtils = new Auth0UserUpdateUtilities(user, auth0, 'incognia');\n\n const status = userUtils.getAppMeta('status');\n // This rule was previously run and calculated the assessment successfully.\n if (status && status !== 'pending') {\n console.log(\n 'Assessment is already calculated or is unevaluable. Skipping.'\n );\n return callback(null, user, context);\n }\n\n let incogniaAPI;\n if (global.incogniaAPI) {\n incogniaAPI = global.incogniaAPI;\n } else {\n incogniaAPI = new IncogniaAPI({\n clientId: INCOGNIA_CLIENT_ID,\n clientSecret: INCOGNIA_CLIENT_SECRET,\n region: INCOGNIA_REGION\n });\n global.incogniaAPI = incogniaAPI;\n }\n\n let onboardingAssessment;\n const signupId = userUtils.getAppMeta('signup_id');\n // The rule was previously run, but Incognia could not assess the signup.\n if (signupId) {\n try {\n onboardingAssessment = await incogniaAPI.getOnboardingAssessment(\n signupId\n );\n } catch (error) {\n console.log('Error calling Incognia API for signup previously submitted');\n return callback(error);\n }\n // This is the first time the rule is being run with all necessary arguments.\n } else {\n try {\n onboardingAssessment = await incogniaAPI.registerOnboardingAssessment({\n installationId: installationId,\n addressLine: homeAddress\n });\n } catch (error) {\n console.log('Error calling Incognia API for new signup submission');\n return callback(error);\n }\n }\n\n /*\n * Updates the status in the metadata now that the assessment was calculated. If the new\n * assessment is valid, the status will go to evaluated and this rule won't be executed again.\n * If Incognia still doesn't know how to assess the signup, it will try to calculate it again up\n * to 48 hours after the first try.\n */\n const firstAssessmentAt = userUtils.getAppMeta('first_assessment_at');\n let newStatus;\n if (onboardingAssessment.riskAssessment !== 'unknown_risk') {\n newStatus = 'evaluated';\n } else if (!firstAssessmentAt) {\n newStatus = 'pending';\n } else {\n const firstAssessmentAge =\n Math.round(Date.now() / 1000) - firstAssessmentAt;\n // 48 hours limit.\n if (firstAssessmentAge > 172800) {\n newStatus = 'unevaluable';\n } else {\n newStatus = 'pending';\n }\n }\n\n const updatedMetadata = {\n status: newStatus,\n first_assessment_at: firstAssessmentAt || Math.round(Date.now() / 1000),\n signup_id: onboardingAssessment.id,\n assessment: onboardingAssessment\n };\n\n try {\n userUtils.setAppMeta('incognia', updatedMetadata);\n await userUtils.updateAppMeta();\n } catch (error) {\n console.log('Error calling Auth0 management API');\n return callback(error);\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "mylife-digital-progressive-consent",
- "title": "Consentric Progressive Consent",
- "overview": "Uses a widget to capture missing consents and preferences at login to boost engagement and support compliance",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the MyLife Digital integration for more information and detailed installation instructions.\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nCONSENTRIC_AUTH_HOST
The URL to authenticate against for your Consentric API token, like https://sandbox-consentric.eu.auth0.com
\nCONSENTRIC_API_HOST
The Consentric API host URL, like https://sandbox.consentric.io
\nCONSENTRIC_CLIENT_ID
The Consentric ClientId issued to you \nCONSENTRIC_CLIENT_SECRET
The Consentric ClientSecret issued to you \nCONSENTRIC_AUDIENCE
The name of the Consentric API being called, like https://sandbox.consentric.io
\nCONSENTRIC_APPLICATION_ID
The Consentric ApplicationId issued to you \nCONSENTRIC_REDIRECT_URL
The URL of the page containing the Progressive widget \n
",
- "code": "function consentricProgressiveConsent(user, context, callback) {\n const axios = require('axios@0.21.1');\n const moment = require('moment@2.11.2');\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const asMilliSeconds = (seconds) => seconds * 1000;\n\n const {\n CONSENTRIC_AUTH_HOST,\n CONSENTRIC_API_HOST,\n CONSENTRIC_AUDIENCE,\n CONSENTRIC_CLIENT_ID,\n CONSENTRIC_CLIENT_SECRET,\n CONSENTRIC_APPLICATION_ID,\n CONSENTRIC_REDIRECT_URL\n } = configuration;\n\n if (\n !CONSENTRIC_AUTH_HOST ||\n !CONSENTRIC_API_HOST ||\n !CONSENTRIC_AUDIENCE ||\n !CONSENTRIC_CLIENT_ID ||\n !CONSENTRIC_CLIENT_SECRET ||\n !CONSENTRIC_APPLICATION_ID ||\n !CONSENTRIC_REDIRECT_URL\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const consentricAuth = axios.create({\n baseURL: CONSENTRIC_AUTH_HOST,\n timeout: 1000\n });\n\n const consentricApi = axios.create({\n baseURL: CONSENTRIC_API_HOST,\n timeout: 1000\n });\n\n // Returns Consentric API Access Token (JWT) from either the global cache or generates it anew from clientId and secret\n const getConsentricApiAccessToken = async () => {\n const consentricApiTokenNotValid =\n !global.consentricApiToken || global.consentricApiToken.exp < Date.now();\n\n if (consentricApiTokenNotValid) {\n try {\n // Exchange Credentials for Consentric Api Access token\n const {\n data: { expires_in, access_token }\n } = await consentricAuth.post('/oauth/token', {\n grant_type: 'client_credentials',\n client_id: CONSENTRIC_CLIENT_ID,\n client_secret: CONSENTRIC_CLIENT_SECRET,\n audience: CONSENTRIC_AUDIENCE,\n applicationId: CONSENTRIC_APPLICATION_ID\n });\n\n const expiryInMs = new Date().getTime() + asMilliSeconds(expires_in);\n const auth = {\n jwt: access_token,\n exp: expiryInMs\n };\n\n // Persist API Access token in global properties\n global.consentricApiToken = auth;\n } catch (error) {\n console.error(\n 'Unable to retrieve API Access token for Consentric. Please check that your credentials (CONSENTRIC_CLIENT_ID and CONSENTRIC_CLIENT_SECRET) are correct.'\n );\n throw error;\n }\n }\n\n return global.consentricApiToken;\n };\n\n // Creates Citizen Record in Consentric with Auth0 Id\n const createCitizen = ({ userRef, apiAccessToken }) => {\n console.log(`Upserting Consentric Citizen record for ${userRef}`);\n const data = {\n applicationId: CONSENTRIC_APPLICATION_ID,\n externalRef: userRef\n };\n\n return consentricApi\n .post('/v1/citizens', data, {\n headers: {\n Authorization: 'Bearer ' + apiAccessToken\n }\n })\n .catch((err) => {\n if (err.response.status !== 409) {\n // 409 indicates Citizen with given reference already exists in Consentric\n console.error(err);\n throw err;\n }\n });\n };\n\n // Function to retrieve Consentric User Token from User Metadata\n const getConsentricUserTokenFromMetadata = (user) =>\n user.app_metadata && user.app_metadata.consentric;\n\n // Generates On Demand Consentric User Token for the given User using the API Access Token\n const generateConsentricUserAccessToken = async ({\n userRef,\n apiAccessToken\n }) => {\n try {\n console.log(`Attempting to generate access token API for ${userRef}`);\n\n const {\n data: { token, expiryDate: exp }\n } = await consentricApi.post(\n '/v1/access-tokens/tokens',\n {\n applicationId: CONSENTRIC_APPLICATION_ID,\n externalRef: userRef,\n expiryDate: moment().add(3, 'months').toISOString()\n },\n {\n headers: {\n Authorization: 'Bearer ' + apiAccessToken\n }\n }\n );\n\n return {\n token,\n exp\n };\n } catch (err) {\n console.error(err);\n throw err;\n }\n };\n\n const loadConsentricUserAccessToken = async ({ user }) => {\n try {\n const metadataUserToken = getConsentricUserTokenFromMetadata(user);\n if (\n metadataUserToken &&\n moment(metadataUserToken.exp).subtract(1, 'days').isAfter(moment())\n )\n return metadataUserToken;\n\n const { jwt: apiAccessToken } = await getConsentricApiAccessToken();\n const apiCredentials = {\n userRef: user.user_id,\n apiAccessToken\n };\n\n // Create Citizen with Auth0 UserId\n await createCitizen(apiCredentials);\n\n // Generate an On Demand Access Token for the created citizen\n const generatedToken = await generateConsentricUserAccessToken(\n apiCredentials\n );\n\n // Persist the app_metadata update\n await auth0.users.updateAppMetadata(user.user_id, {\n consentric: generatedToken\n });\n\n return generatedToken;\n } catch (err) {\n console.error(\n `Issue loading Consentric User Access Token for user ${user.user_id} - ${err}`\n );\n throw err;\n }\n };\n\n const initConsentricFlow = async () => {\n try {\n const { token } = await loadConsentricUserAccessToken({ user });\n const urlConnector = CONSENTRIC_REDIRECT_URL.includes('?') ? '&' : '?';\n const redirectUrl =\n CONSENTRIC_REDIRECT_URL + urlConnector + 'token=' + token;\n\n context.redirect = {\n url: redirectUrl\n };\n } catch (err) {\n console.error(`CONSENTRIC RULE ABORTED: ${err}`);\n }\n return callback(null, user, context);\n };\n\n if (ruleUtils.canRedirect) {\n return initConsentricFlow();\n } else {\n // Run after Redirect or Silent Auth\n return callback(null, user, context);\n }\n}"
- },
- {
- "id": "netlify-role-management",
- "title": "Netlify Role Management",
- "overview": "Adds a default role if the user doesn't have any yet and attaches roles to the ID Token.",
- "categories": [
- "marketplace"
- ],
- "description": "Optional configuration:
\n\nDEFAULT_ROLE_NAME
- name of the default role to be given to a user \nDEFAULT_ROLE_ID
- id of the role to be given to a user \nCUSTOM_CLAIMS_NAMESPACE
- namespace for adding custom claims to ID Token \n
",
- "code": "async function netlifyRoleManagement(user, context, callback) {\n const ManagementClient = require('auth0@2.30.0').ManagementClient;\n\n const namespace =\n configuration.CUSTOM_CLAIMS_NAMESPACE || 'https://netlify-integration.com';\n const assignedRoles = (context.authorization || {}).roles || [];\n const defaultRoleName = configuration.DEFAULT_ROLE_NAME;\n const defaultRoleId = configuration.DEFAULT_ROLE_ID;\n\n //give default role if the user doesn't already have any roles assigned\n if (\n (!assignedRoles || assignedRoles.length === 0) &&\n defaultRoleName &&\n defaultRoleId\n ) {\n try {\n const management = new ManagementClient({\n token: auth0.accessToken,\n domain: auth0.domain\n });\n await management.assignRolestoUser(\n { id: user.user_id },\n { roles: [defaultRoleId] }\n );\n } catch (ex) {\n console.error('Failed to add default role to user', ex);\n } finally {\n assignedRoles.push(defaultRoleName);\n }\n }\n\n context.idToken[namespace + '/roles'] = assignedRoles;\n return callback(null, user, context);\n}"
- },
- {
- "id": "onetrust-consent-management",
- "title": "OneTrust Consent Management",
- "overview": "Enhance Auth0 user profiles with consent, opt-ins and communication preferences data.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the OneTrust integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nONETRUST_REQUEST_INFORMATION
Your OneTrust Collection Point API token \nONETRUST_CONSENT_API_URL
Your OneTrust Collection Point API URL \nONETRUST_PURPOSE_ID
Your OneTrust Collection Point Purpose ID \n
\nOptional configuration:
\n\nONETRUST_SKIP_IF_NO_EMAIL
If set to \"true\" then the Rule will be skipped if there is no email address. Otherwise the Rule will fail with an error. \n
",
- "code": "/* global configuration */\nasync function oneTrustConsentManagement(user, context, callback) {\n const axios = require('axios@0.21.1');\n\n const {\n ONETRUST_REQUEST_INFORMATION,\n ONETRUST_CONSENT_API_URL,\n ONETRUST_PURPOSE_ID\n } = configuration;\n\n if (\n !ONETRUST_REQUEST_INFORMATION ||\n !ONETRUST_CONSENT_API_URL ||\n !ONETRUST_PURPOSE_ID\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n const skipIfNoEmail = configuration.ONETRUST_SKIP_IF_NO_EMAIL === 'true';\n\n user.app_metadata = user.app_metadata || {};\n let onetrust = user.app_metadata.onetrust || {};\n\n if (onetrust.receipt) {\n console.log('User has a Collection Point receipt. Skipping.');\n return callback(null, user, context);\n }\n\n if (!user.email) {\n if (skipIfNoEmail) {\n console.log('User has no email address. Skipping.');\n return callback(null, user, context);\n }\n return callback(new Error('An email address is required.'));\n }\n\n try {\n const response = await axios.post(ONETRUST_CONSENT_API_URL, {\n identifier: user.email,\n requestInformation: ONETRUST_REQUEST_INFORMATION,\n purposes: [{ Id: ONETRUST_PURPOSE_ID }]\n });\n onetrust.receipt = response.data.receipt;\n } catch (error) {\n console.log('Error calling the Collection Point.');\n return callback(error);\n }\n\n try {\n await auth0.users.updateAppMetadata(user.user_id, { onetrust });\n } catch (error) {\n console.log('Error updating user app_metadata.');\n return callback(error);\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "onfido-idv",
- "title": "Onfido Identity Verification",
- "overview": "Redirect to your Onfido IDV Application for Identity Verification during login.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Onfido integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nSESSION_TOKEN_SECRET
Long, random string, should match on Onfido app side. \nONFIDO_API_TOKEN
Your Onfido API Token \nONFIDO_REGION
The supported Onfido region your tenant is operating in \nONFIDO_ID_VERIFICATION_URL
URL to receive the redirect \n
",
- "code": "/* global configuration */\nasync function onfidoIdentityVerification(user, context, callback) {\n if (\n !configuration.SESSION_TOKEN_SECRET ||\n !configuration.ONFIDO_API_TOKEN ||\n !configuration.ONFIDO_REGION ||\n !configuration.ONFIDO_ID_VERIFICATION_URL\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n // using auth0 rule-utilities to make sure our rule is efficient in the pipeline\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n // requiring Onfido's node SDK for making the calls easier to Onfido's service.\n const { Onfido, Region } = require('@onfido/api@1.5.1');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n // creating a claim namespace for adding the Onfido IDV check results back to the ID Token\n const claimNamespace = 'https://claims.onfido.com/';\n\n // creating a new Onfido client, the region here is where your Onfido instance is located. Possible values are EU for Europe, US for United States, and CA for Canada.\n const onfidoClient = new Onfido({\n apiToken: configuration.ONFIDO_API_TOKEN,\n region: Region[configuration.ONFIDO_REGION] || Region.EU\n });\n\n user.app_metadata = user.app_metadata || {};\n user.app_metadata.onfido = user.app_metadata.onfido || {};\n\n if (\n ruleUtils.isRedirectCallback &&\n ruleUtils.queryParams.session_token &&\n 'true' === ruleUtils.queryParams.onfido_idv\n ) {\n // User is back from the Onfido experience and has a session token to validate and assign to user meta\n\n // Validating session token and extracting payload for check results\n let payload;\n try {\n payload = ruleUtils.validateSessionToken();\n } catch (error) {\n return callback(error);\n }\n\n // assigning check status and result to the app_metadata so the downstream application can decided what to do next\n // note, in the example integration, the Onfido app returns after 30 seconds even if the check is still in progress\n // If this claim status is still in_progress it is recommended the downstream application recheck for completion or implement the Onfido Webhook: https://documentation.onfido.com/#webhooks\n // Additionally, you can place these items into the idToken claim with custom claims as needed as shown\n const onfido = {\n check_result: payload.checkResult,\n check_status: payload.checkStatus,\n applicant_id: payload.applicant\n };\n try {\n await auth0.users.updateAppMetadata(user.user_id, onfido);\n } catch (error) {\n callback(error);\n }\n\n user.app_metadata.onfido = onfido;\n\n context.idToken[claimNamespace + 'check_result'] = payload.checkResult;\n context.idToken[claimNamespace + 'check_status'] = payload.checkStatus;\n context.idToken[claimNamespace + 'applicant_id'] = payload.applicant;\n\n return callback(null, user, context);\n }\n\n if (ruleUtils.canRedirect && !user.app_metadata.onfido.check_status) {\n // if the user has not already been redirected and check_status is empty, we will create the applicant and redirect to the Onfido implementation.\n let applicant;\n try {\n applicant = await onfidoClient.applicant.create({\n // these values do not need to match what is on the document for IDV, but if Data Comparison on Onfido's side is tuned on, these values will flag\n // if Auth0 contains these values in the app_metadata or on the user object you can map them here as needed. You could also pass them in as query_string variables\n firstName: !user.given_name ? 'anon' : user.given_name,\n lastName: !user.family_name ? 'anon' : user.family_name,\n email: !user.email ? 'anon@example.com' : user.email\n });\n\n // create the session token with the applicant id as a custom claim\n const sessionToken = ruleUtils.createSessionToken({\n applicant: applicant.id\n });\n // redirect to Onfido implementation with sessionToken\n ruleUtils.doRedirect(\n configuration.ONFIDO_ID_VERIFICATION_URL,\n sessionToken\n );\n return callback(null, user, context);\n } catch (error) {\n return callback(error);\n }\n }\n return callback(null, user, context);\n}"
- },
- {
- "id": "scaled-access-relationships-claim",
- "title": "Scaled Access relationship-based claims",
- "overview": "Adds a claim based on the relationships the subject has in Scaled Access",
- "categories": [
- "marketplace"
- ],
- "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.) \nSECZETTA_PROFILE_TYPE_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://{{SECZETTA_BASE_URL}}/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",
- "overview": "Verify a person's identity using Vouched.",
- "categories": [
- "marketplace"
- ],
- "description": "Please see the Vouched integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nVOUCHED_API_KEY
Your Private Key located in Vouched Dashboard \nVOUCHED_PUBLIC_KEY
Your Public Key located in Vouched Dashboard \n
\nOptional configuration:
\n\nVOUCHED_API_URL
Your Vouched API URL; leave blank unless instructed by your Vouched rep \nVOUCHED_ID_TOKEN_CLAIM
Set a https://vouchedid/is_verified
claim in the ID token with results \nVOUCHED_VERIFICATION_OPTIONAL
Set to \"true\" to succeed even if verification fails \n
",
- "code": "async function vouchedVerification(user, context, callback) {\n if (!configuration.VOUCHED_API_KEY || !configuration.VOUCHED_PUBLIC_KEY) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n /* ----------- START helpers ----------- */\n const axios = require('axios');\n const url = require('url');\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const defaultApiUrl = 'https://verify.vouched.id/api';\n const defaultUiUrl = 'https://i.vouched.id';\n const idTokenClaim = 'https://vouched.id/is_verified';\n\n const getJobByToken = async (apiKey, jobToken, apiUrl) => {\n return getJob(apiKey, { token: jobToken }, apiUrl);\n };\n\n const getJobById = async (apiKey, jobId, apiUrl) => {\n return getJob(apiKey, { id: jobId }, apiUrl);\n };\n\n const getJob = async (apiKey, params, apiUrl) => {\n const response = await axios({\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json'\n },\n baseURL: apiUrl,\n url: '/jobs',\n params: params\n });\n const items = response.data.items;\n if (items.length === 0) {\n throw new Error(\n `Unable to find Job with the following params: ${JSON.stringify(\n params\n )}`\n );\n }\n return items[0];\n };\n\n const createPacket = async (\n apiKey,\n publicKey,\n continueUrl,\n user,\n apiUrl = defaultApiUrl\n ) => {\n const requestBody = {\n pk: publicKey,\n uid: user.user_id,\n continueUrl\n };\n\n if (user.given_name) requestBody.firstName = user.given_name;\n\n if (user.family_name) requestBody.lastName = user.family_name;\n\n const response = await axios({\n method: 'post',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json'\n },\n baseURL: apiUrl,\n url: '/packet/auth0',\n data: requestBody\n });\n const data = response.data;\n if (data.errors) {\n throw new Error(`${data.errors[0].message}`);\n }\n return data.id;\n };\n\n const isJobForUser = (job, userId) => {\n try {\n return (\n job.request.properties.filter(\n (prop) => prop.name === 'uid' && prop.value === userId\n ).length === 1\n );\n } catch (e) {\n return false;\n }\n };\n\n const extractResults = (job) => {\n const { id, status, reviewSuccess, result } = job;\n return {\n id,\n status,\n reviewSuccess,\n result\n };\n };\n\n const isJobVerified = (job) => {\n try {\n return job.result.success || job.reviewSuccess;\n } catch (e) {\n return false;\n }\n };\n\n const redirectToVerification = (packetId, baseUrl = defaultUiUrl) => {\n const redirectUrl = new url.URL(`${baseUrl}/auth0`);\n redirectUrl.searchParams.append('id', packetId);\n return redirectUrl.href;\n };\n\n /* ----------- END helpers ----------- */\n\n user.app_metadata = user.app_metadata || {};\n const vouchedApiUrl = configuration.VOUCHED_API_URL || defaultApiUrl;\n\n try {\n const jobToken = ruleUtils.queryParams.jobToken;\n if (ruleUtils.isRedirectCallback && jobToken) {\n // get job from API\n const job = await getJobByToken(\n configuration.VOUCHED_API_KEY,\n jobToken,\n vouchedApiUrl\n );\n\n // check if job's user is the same as current user\n if (!isJobForUser(job, user.user_id)) {\n return callback(\n new Error(`The ID Verification results do not belong to this user.`)\n );\n }\n\n // update app metadata w/ results\n user.app_metadata.vouched = extractResults(job);\n await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);\n }\n\n const vouchedResults = user.app_metadata.vouched;\n if (vouchedResults) {\n if (!isJobVerified(vouchedResults)) {\n // user failed id verification\n const mostRecentJob = await getJobById(\n configuration.VOUCHED_API_KEY,\n vouchedResults.id,\n vouchedApiUrl\n );\n\n // check if job's user is the same as current user\n if (!isJobForUser(mostRecentJob, user.user_id)) {\n return callback(\n new Error(`The ID Verification results do not belong to this user.`)\n );\n }\n\n // user is now verified, update app metadata\n if (isJobVerified(mostRecentJob)) {\n user.app_metadata.vouched = extractResults(mostRecentJob);\n await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);\n } else {\n // user failed verification check and doesn't have an override\n if (configuration.VOUCHED_ID_TOKEN_CLAIM === 'true') {\n context.idToken[idTokenClaim] = false;\n }\n if (configuration.VOUCHED_VERIFICATION_OPTIONAL === 'true') {\n return callback(null, user, context);\n }\n\n return callback(new Error(`This user's ID cannot be verified.`));\n }\n }\n } else {\n // create Auth0 packet to securely pass info to Vouched\n const packetId = await createPacket(\n configuration.VOUCHED_API_KEY,\n configuration.VOUCHED_PUBLIC_KEY,\n `https://${context.request.hostname}/continue`,\n user\n );\n\n // user doesn't have a verification result, redirect to Vouched with packet\n if (ruleUtils.canRedirect) {\n context.redirect = { url: redirectToVerification(packetId) };\n }\n return callback(null, user, context);\n }\n } catch (e) {\n return callback(e);\n }\n\n if (configuration.VOUCHED_ID_TOKEN_CLAIM === 'true') {\n context.idToken[idTokenClaim] = true;\n }\n\n return callback(null, user, context);\n}"
- },
- {
- "id": "yoonik-face",
- "title": "YooniK Face Authentication",
- "overview": "Redirect to your YooniK Face Application for Face Authentication during login.",
- "categories": [
- "marketplace"
- ],
- "description": "Required configuration (this Rule will be skipped if any of the below are not defined):
\n\nSESSION_TOKEN_SECRET
A random long string that is used to sign the session token sent\n to the custom app implementing YooniK Face Capture SDK. This value will also need to be\n used in the custom app in order to verify and re-sign the session token back to Auth0. \nYOONIK_APP_URL
The URL of your custom application that receives the redirect. \n
",
- "code": "async function yoonikFaceAuthentication(user, context, callback) {\n if (!configuration.SESSION_TOKEN_SECRET || !configuration.YOONIK_APP_URL) {\n console.log('Please set required configuration parameters.');\n return callback(null, user, context);\n }\n\n const {\n Auth0RedirectRuleUtilities,\n Auth0UserUpdateUtilities\n } = require('@auth0/rule-utilities@0.2.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const userUtils = new Auth0UserUpdateUtilities(user, auth0, 'yoonik');\n\n const claimNamespace = 'https://claims.yoonik.me/';\n\n if (\n ruleUtils.isRedirectCallback &&\n ruleUtils.queryParams.session_token &&\n ruleUtils.queryParams.yoonik_authentication === 'true'\n ) {\n // User is back from the YooniK redirect and has a session token to validate.\n let payload;\n try {\n payload = ruleUtils.validateSessionToken();\n } catch (error) {\n callback(error);\n }\n\n userUtils.setAppMeta('status', payload.status);\n\n try {\n await userUtils.updateAppMeta();\n } catch (error) {\n callback(error);\n }\n\n context.idToken[claimNamespace + 'status'] = payload.status;\n\n callback(null, user, context);\n }\n\n if (ruleUtils.canRedirect) {\n try {\n ruleUtils.doRedirect(configuration.YOONIK_APP_URL);\n callback(null, user, context);\n } catch (error) {\n callback(error);\n }\n }\n\n return callback(null, user, context);\n}"
- }
- ]
- },
{
"name": "webhook",
"templates": [
@@ -674,7 +519,7 @@
"webhook"
],
"description": "This rule will send an email to an administrator on a user's first login. We use a persistent signedUp
property to track whether this is the case or not.
\nThis rule assumes you've stored a secure value named MANDRILL_API_KEY
, which contains your secret API key for Mandrill. It will be sent with each request.
\nIn the same way, other services such as Amazon SES and SendGrid can be used.
\nMake sure to change the sender and destination emails.
",
- "code": "function sendMandrillEmail(user, context, callback) {\n const request = require('request');\n\n user.app_metadata = user.app_metadata || {};\n // Only send an email when user signs up\n if (user.app_metadata.signedUp) {\n return callback(null, user, context);\n }\n\n // See https://mandrillapp.com/api/docs/messages.JSON.html#method=send\n const body = {\n key: configuration.MANDRILL_API_KEY,\n message: {\n subject: 'User ' + user.name + ' signed up to ' + context.clientName,\n text: 'Sent from an Auth0 rule',\n from_email: 'SENDER_EMAIL@example.com',\n from_name: 'Auth0 Rule',\n to: [\n {\n email: 'DESTINATION_EMAIL@example.com',\n type: 'to'\n }\n ]\n }\n };\n const mandrill_send_endpoint =\n 'https://mandrillapp.com/api/1.0/messages/send.json';\n\n request.post({ url: mandrill_send_endpoint, form: body }, function (\n err,\n resp,\n body\n ) {\n if (err) {\n return callback(err);\n }\n user.app_metadata.signedUp = true;\n auth0.users\n .updateAppMetadata(user.user_id, user.app_metadata)\n .then(function () {\n callback(null, user, context);\n })\n .catch(function (err) {\n callback(err);\n });\n });\n}"
+ "code": "function sendMandrillEmail(user, context, callback) {\n const request = require('request');\n\n user.app_metadata = user.app_metadata || {};\n // Only send an email when user signs up\n if (user.app_metadata.signedUp) {\n return callback(null, user, context);\n }\n\n // See https://mandrillapp.com/api/docs/messages.JSON.html#method=send\n const body = {\n key: configuration.MANDRILL_API_KEY,\n message: {\n subject: 'User ' + user.name + ' signed up to ' + context.clientName,\n text: 'Sent from an Auth0 rule',\n from_email: 'SENDER_EMAIL@example.com',\n from_name: 'Auth0 Rule',\n to: [\n {\n email: 'DESTINATION_EMAIL@example.com',\n type: 'to'\n }\n ]\n }\n };\n const mandrill_send_endpoint =\n 'https://mandrillapp.com/api/1.0/messages/send.json';\n\n request.post(\n { url: mandrill_send_endpoint, form: body },\n function (err, resp, body) {\n if (err) {\n return callback(err);\n }\n user.app_metadata.signedUp = true;\n auth0.users\n .updateAppMetadata(user.user_id, user.app_metadata)\n .then(function () {\n callback(null, user, context);\n })\n .catch(function (err) {\n callback(err);\n });\n }\n );\n}"
},
{
"id": "mixpanel-track-event",
@@ -816,6 +661,41 @@
}
]
},
+ {
+ "name": "marketplace",
+ "templates": [
+ {
+ "id": "netlify-role-management",
+ "title": "Netlify Role Management",
+ "overview": "Adds a default role if the user doesn't have any yet and attaches roles to the ID Token.",
+ "categories": [
+ "marketplace"
+ ],
+ "description": "Optional configuration:
\n\nDEFAULT_ROLE_NAME
- name of the default role to be given to a user \nDEFAULT_ROLE_ID
- id of the role to be given to a user \nCUSTOM_CLAIMS_NAMESPACE
- namespace for adding custom claims to ID Token \n
",
+ "code": "async function netlifyRoleManagement(user, context, callback) {\n const ManagementClient = require('auth0@2.30.0').ManagementClient;\n\n const namespace =\n configuration.CUSTOM_CLAIMS_NAMESPACE || 'https://netlify-integration.com';\n const assignedRoles = (context.authorization || {}).roles || [];\n const defaultRoleName = configuration.DEFAULT_ROLE_NAME;\n const defaultRoleId = configuration.DEFAULT_ROLE_ID;\n\n //give default role if the user doesn't already have any roles assigned\n if (\n (!assignedRoles || assignedRoles.length === 0) &&\n defaultRoleName &&\n defaultRoleId\n ) {\n try {\n const management = new ManagementClient({\n token: auth0.accessToken,\n domain: auth0.domain\n });\n await management.assignRolestoUser(\n { id: user.user_id },\n { roles: [defaultRoleId] }\n );\n } catch (ex) {\n console.error('Failed to add default role to user', ex);\n } finally {\n assignedRoles.push(defaultRoleName);\n }\n }\n\n context.idToken[namespace + '/roles'] = assignedRoles;\n return callback(null, user, context);\n}"
+ },
+ {
+ "id": "onfido-idv",
+ "title": "Onfido Identity Verification",
+ "overview": "Redirect to your Onfido IDV Application for Identity Verification during login.",
+ "categories": [
+ "marketplace"
+ ],
+ "description": "Please see the Onfido integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nSESSION_TOKEN_SECRET
Long, random string, should match on Onfido app side. \nONFIDO_API_TOKEN
Your Onfido API Token \nONFIDO_REGION
The supported Onfido region your tenant is operating in \nONFIDO_ID_VERIFICATION_URL
URL to receive the redirect \n
",
+ "code": "/* global configuration */\nasync function onfidoIdentityVerification(user, context, callback) {\n if (\n !configuration.SESSION_TOKEN_SECRET ||\n !configuration.ONFIDO_API_TOKEN ||\n !configuration.ONFIDO_REGION ||\n !configuration.ONFIDO_ID_VERIFICATION_URL\n ) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n // using auth0 rule-utilities to make sure our rule is efficient in the pipeline\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n // requiring Onfido's node SDK for making the calls easier to Onfido's service.\n const { Onfido, Region } = require('@onfido/api@1.5.1');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n // creating a claim namespace for adding the Onfido IDV check results back to the ID Token\n const claimNamespace = 'https://claims.onfido.com/';\n\n // creating a new Onfido client, the region here is where your Onfido instance is located. Possible values are EU for Europe, US for United States, and CA for Canada.\n const onfidoClient = new Onfido({\n apiToken: configuration.ONFIDO_API_TOKEN,\n region: Region[configuration.ONFIDO_REGION] || Region.EU\n });\n\n user.app_metadata = user.app_metadata || {};\n user.app_metadata.onfido = user.app_metadata.onfido || {};\n\n if (\n ruleUtils.isRedirectCallback &&\n ruleUtils.queryParams.session_token &&\n 'true' === ruleUtils.queryParams.onfido_idv\n ) {\n // User is back from the Onfido experience and has a session token to validate and assign to user meta\n\n // Validating session token and extracting payload for check results\n let payload;\n try {\n payload = ruleUtils.validateSessionToken();\n } catch (error) {\n return callback(error);\n }\n\n // assigning check status and result to the app_metadata so the downstream application can decided what to do next\n // note, in the example integration, the Onfido app returns after 30 seconds even if the check is still in progress\n // If this claim status is still in_progress it is recommended the downstream application recheck for completion or implement the Onfido Webhook: https://documentation.onfido.com/#webhooks\n // Additionally, you can place these items into the idToken claim with custom claims as needed as shown\n const onfido = {\n check_result: payload.checkResult,\n check_status: payload.checkStatus,\n applicant_id: payload.applicant\n };\n try {\n await auth0.users.updateAppMetadata(user.user_id, onfido);\n } catch (error) {\n callback(error);\n }\n\n user.app_metadata.onfido = onfido;\n\n context.idToken[claimNamespace + 'check_result'] = payload.checkResult;\n context.idToken[claimNamespace + 'check_status'] = payload.checkStatus;\n context.idToken[claimNamespace + 'applicant_id'] = payload.applicant;\n\n return callback(null, user, context);\n }\n\n if (ruleUtils.canRedirect && !user.app_metadata.onfido.check_status) {\n // if the user has not already been redirected and check_status is empty, we will create the applicant and redirect to the Onfido implementation.\n let applicant;\n try {\n applicant = await onfidoClient.applicant.create({\n // these values do not need to match what is on the document for IDV, but if Data Comparison on Onfido's side is tuned on, these values will flag\n // if Auth0 contains these values in the app_metadata or on the user object you can map them here as needed. You could also pass them in as query_string variables\n firstName: !user.given_name ? 'anon' : user.given_name,\n lastName: !user.family_name ? 'anon' : user.family_name,\n email: !user.email ? 'anon@example.com' : user.email\n });\n\n // create the session token with the applicant id as a custom claim\n const sessionToken = ruleUtils.createSessionToken({\n applicant: applicant.id\n });\n // redirect to Onfido implementation with sessionToken\n ruleUtils.doRedirect(\n configuration.ONFIDO_ID_VERIFICATION_URL,\n sessionToken\n );\n return callback(null, user, context);\n } catch (error) {\n return callback(error);\n }\n }\n return callback(null, user, context);\n}"
+ },
+ {
+ "id": "vouched-verification",
+ "title": "Vouched Verification",
+ "overview": "Verify a person's identity using Vouched.",
+ "categories": [
+ "marketplace"
+ ],
+ "description": "Please see the Vouched integration for more information and detailed installation instructions.
\nRequired configuration (this Rule will be skipped if any of the below are not defined):
\n\nVOUCHED_API_KEY
Your Private Key located in Vouched Dashboard \nVOUCHED_PUBLIC_KEY
Your Public Key located in Vouched Dashboard \n
\nOptional configuration:
\n\nVOUCHED_API_URL
Your Vouched API URL; leave blank unless instructed by your Vouched rep \nVOUCHED_ID_TOKEN_CLAIM
Set a https://vouchedid/is_verified
claim in the ID token with results \nVOUCHED_VERIFICATION_OPTIONAL
Set to \"true\" to succeed even if verification fails \n
",
+ "code": "async function vouchedVerification(user, context, callback) {\n if (!configuration.VOUCHED_API_KEY || !configuration.VOUCHED_PUBLIC_KEY) {\n console.log('Missing required configuration. Skipping.');\n return callback(null, user, context);\n }\n\n /* ----------- START helpers ----------- */\n const axios = require('axios');\n const url = require('url');\n const { Auth0RedirectRuleUtilities } = require('@auth0/rule-utilities@0.1.0');\n\n const ruleUtils = new Auth0RedirectRuleUtilities(\n user,\n context,\n configuration\n );\n\n const defaultApiUrl = 'https://verify.vouched.id/api';\n const defaultUiUrl = 'https://i.vouched.id';\n const idTokenClaim = 'https://vouched.id/is_verified';\n\n const getJobByToken = async (apiKey, jobToken, apiUrl) => {\n return getJob(apiKey, { token: jobToken }, apiUrl);\n };\n\n const getJobById = async (apiKey, jobId, apiUrl) => {\n return getJob(apiKey, { id: jobId }, apiUrl);\n };\n\n const getJob = async (apiKey, params, apiUrl) => {\n const response = await axios({\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json'\n },\n baseURL: apiUrl,\n url: '/jobs',\n params: params\n });\n const items = response.data.items;\n if (items.length === 0) {\n throw new Error(\n `Unable to find Job with the following params: ${JSON.stringify(\n params\n )}`\n );\n }\n return items[0];\n };\n\n const createPacket = async (\n apiKey,\n publicKey,\n continueUrl,\n user,\n apiUrl = defaultApiUrl\n ) => {\n const requestBody = {\n pk: publicKey,\n uid: user.user_id,\n continueUrl\n };\n\n if (user.given_name) requestBody.firstName = user.given_name;\n\n if (user.family_name) requestBody.lastName = user.family_name;\n\n const response = await axios({\n method: 'post',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json'\n },\n baseURL: apiUrl,\n url: '/packet/auth0',\n data: requestBody\n });\n const data = response.data;\n if (data.errors) {\n throw new Error(`${data.errors[0].message}`);\n }\n return data.id;\n };\n\n const isJobForUser = (job, userId) => {\n try {\n return (\n job.request.properties.filter(\n (prop) => prop.name === 'uid' && prop.value === userId\n ).length === 1\n );\n } catch (e) {\n return false;\n }\n };\n\n const extractResults = (job) => {\n const { id, status, reviewSuccess, result } = job;\n return {\n id,\n status,\n reviewSuccess,\n result\n };\n };\n\n const isJobVerified = (job) => {\n try {\n return job.result.success || job.reviewSuccess;\n } catch (e) {\n return false;\n }\n };\n\n const redirectToVerification = (packetId, baseUrl = defaultUiUrl) => {\n const redirectUrl = new url.URL(`${baseUrl}/auth0`);\n redirectUrl.searchParams.append('id', packetId);\n return redirectUrl.href;\n };\n\n /* ----------- END helpers ----------- */\n\n user.app_metadata = user.app_metadata || {};\n const vouchedApiUrl = configuration.VOUCHED_API_URL || defaultApiUrl;\n\n try {\n const jobToken = ruleUtils.queryParams.jobToken;\n if (ruleUtils.isRedirectCallback && jobToken) {\n // get job from API\n const job = await getJobByToken(\n configuration.VOUCHED_API_KEY,\n jobToken,\n vouchedApiUrl\n );\n\n // check if job's user is the same as current user\n if (!isJobForUser(job, user.user_id)) {\n return callback(\n new Error(`The ID Verification results do not belong to this user.`)\n );\n }\n\n // update app metadata w/ results\n user.app_metadata.vouched = extractResults(job);\n await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);\n }\n\n const vouchedResults = user.app_metadata.vouched;\n if (vouchedResults) {\n if (!isJobVerified(vouchedResults)) {\n // user failed id verification\n const mostRecentJob = await getJobById(\n configuration.VOUCHED_API_KEY,\n vouchedResults.id,\n vouchedApiUrl\n );\n\n // check if job's user is the same as current user\n if (!isJobForUser(mostRecentJob, user.user_id)) {\n return callback(\n new Error(`The ID Verification results do not belong to this user.`)\n );\n }\n\n // user is now verified, update app metadata\n if (isJobVerified(mostRecentJob)) {\n user.app_metadata.vouched = extractResults(mostRecentJob);\n await auth0.users.updateAppMetadata(user.user_id, user.app_metadata);\n } else {\n // user failed verification check and doesn't have an override\n if (configuration.VOUCHED_ID_TOKEN_CLAIM === 'true') {\n context.idToken[idTokenClaim] = false;\n }\n if (configuration.VOUCHED_VERIFICATION_OPTIONAL === 'true') {\n return callback(null, user, context);\n }\n\n return callback(new Error(`This user's ID cannot be verified.`));\n }\n }\n } else {\n // create Auth0 packet to securely pass info to Vouched\n const packetId = await createPacket(\n configuration.VOUCHED_API_KEY,\n configuration.VOUCHED_PUBLIC_KEY,\n `https://${context.request.hostname}/continue`,\n user\n );\n\n // user doesn't have a verification result, redirect to Vouched with packet\n if (ruleUtils.canRedirect) {\n context.redirect = { url: redirectToVerification(packetId) };\n }\n return callback(null, user, context);\n }\n } catch (e) {\n return callback(e);\n }\n\n if (configuration.VOUCHED_ID_TOKEN_CLAIM === 'true') {\n context.idToken[idTokenClaim] = true;\n }\n\n return callback(null, user, context);\n}"
+ }
+ ]
+ },
{
"name": "debugging",
"templates": [
diff --git a/src/rules/access-on-weekdays-only-for-an-app.js b/src/rules/access-on-weekdays-only-for-an-app.js
index 00b30812..6486b234 100644
--- a/src/rules/access-on-weekdays-only-for-an-app.js
+++ b/src/rules/access-on-weekdays-only-for-an-app.js
@@ -1,6 +1,6 @@
/**
* This rule is used to prevent access during weekends for a specific app.
- *
+ *
* @title Allow Access during weekdays for a specific App
* @overview Prevent access to app during weekends.
* @gallery true
diff --git a/src/rules/active-directory-groups.js b/src/rules/active-directory-groups.js
index 9fb82651..f2eaf0f4 100644
--- a/src/rules/active-directory-groups.js
+++ b/src/rules/active-directory-groups.js
@@ -2,7 +2,7 @@
* This rule checks if a user belongs to an AD group and if not, it will return Access Denied.
*
* > Note: you can mix this with `context.clientID` or `clientName` to do it only for specific application
- *
+ *
* @title Active Directory group membership
* @overview Check Active Directory membership, else return Access Denied.
* @gallery true
diff --git a/src/rules/caisson-id-check.js b/src/rules/caisson-id-check.js
index 225bdf09..694da97d 100644
--- a/src/rules/caisson-id-check.js
+++ b/src/rules/caisson-id-check.js
@@ -10,7 +10,7 @@
* **Optional configuration:**
*
* - `CAISSON_DEBUG` Set to "true" to log errors in the console
- *
+ *
* @title Caisson ID Check
* @overview Validate US driver's licenses and international passports in real time.
* @gallery false
diff --git a/src/rules/check-domains-against-connection-aliases.js b/src/rules/check-domains-against-connection-aliases.js
index 8cd134d0..cb651c69 100644
--- a/src/rules/check-domains-against-connection-aliases.js
+++ b/src/rules/check-domains-against-connection-aliases.js
@@ -5,7 +5,7 @@
* Use this rule to only allow users from specific email domains to login.
*
* For example, ExampleCo has setup exampleco.com as a managed domain. They add exampleco.com to the email domains list in their SAML connection. Now, only users with an email ending with @exampleco.com (and not @examplecocorp.com) can login via SAML.
- *
+ *
* @title Check if user email domain matches configured domain
* @overview Check user email domain matches domains configured in connection.
* @gallery true
diff --git a/src/rules/create-new-contact-add-to-contact-list-hubspot.js b/src/rules/create-new-contact-add-to-contact-list-hubspot.js
index 422724a0..a95bea91 100644
--- a/src/rules/create-new-contact-add-to-contact-list-hubspot.js
+++ b/src/rules/create-new-contact-add-to-contact-list-hubspot.js
@@ -7,7 +7,7 @@
* For more details about the Rules configuration settings, see here https://auth0.com/docs/rules/guides/configuration
* For more information about Hubspot API keys see here https://knowledge.hubspot.com/integrations/how-do-i-get-my-hubspot-api-key
* Use 1 as the value for HUBSPOT_NEW_MEMBER_LIST_ID for the default list in Hubspot. Otherwise, you can see the ID of any list in HubSpot visiting it, and looking at the URL. It will have this format https://app.hubspot.com/contacts/:portalId/lists/:listId where :listId is the value you want.
- *
+ *
* @title Add New Contact to HubSpot for Marketing
* @overview Add New Contact to HubSpot then add to a List for marketing
* @gallery true
diff --git a/src/rules/cumulio-add-metadata-to-tokens.js b/src/rules/cumulio-add-metadata-to-tokens.js
index d7ddfe8c..680aff77 100644
--- a/src/rules/cumulio-add-metadata-to-tokens.js
+++ b/src/rules/cumulio-add-metadata-to-tokens.js
@@ -3,7 +3,7 @@
* This integration simplifies the process of making full use of integrated Cumul.io dashboards' multi tenant features
* by using Auth0 as its authentication layer. The integration will allow you to set up and use user
* information in Auth0 as Cumul.io parameters to filter and structure your Cumul.io dashboards.
- *
+ *
* @title User app_metadata for Cumul.io
* @overview Add Cumul.io user app_metadata to tokens to be used for Cumul.io dashboard filtering
* @gallery false
diff --git a/src/rules/eva-voice-biometric.js b/src/rules/eva-voice-biometric.js
index 7534baae..046a048d 100644
--- a/src/rules/eva-voice-biometric.js
+++ b/src/rules/eva-voice-biometric.js
@@ -14,7 +14,7 @@
* - `AURAYA_COMMON_DIGITS_PROMPT` A digit string to prompt for common digits (e.g '987654321')
* - `AURAYA_PERSONAL_DIGITS_PROMPT` A string to prompt for personal digits (e.g 'your cell number')
* - `AURAYA_DEBUG` Set to "true" to log errors in the console
- *
+ *
* @title EVA Voice Biometric connector
* @overview EVA Voice Biometric connector rule for Auth0 enables voice enrolment and verification as a second factor
* @gallery false
diff --git a/src/rules/get-fullcontact-profile.js b/src/rules/get-fullcontact-profile.js
index d460c2ba..e0960065 100644
--- a/src/rules/get-fullcontact-profile.js
+++ b/src/rules/get-fullcontact-profile.js
@@ -9,7 +9,7 @@
* **Required configuration** (this Rule will be skipped if any of the below are not defined):
*
* - `FULLCONTACT_KEY` API key found at https://dashboard.fullcontact.com/
- *
+ *
* @title Enrich profile with FullContact
* @overview Get the user profile from FullContact using the email then add a new property to user_metadata.
* @gallery true
diff --git a/src/rules/guardian-multifactor-ip-range.js b/src/rules/guardian-multifactor-ip-range.js
index 062fad91..1a11b236 100644
--- a/src/rules/guardian-multifactor-ip-range.js
+++ b/src/rules/guardian-multifactor-ip-range.js
@@ -1,7 +1,7 @@
/**
*
* This rule is used to trigger multifactor authentication when the requesting IP is from outside the corporate IP range.
- *
+ *
* @title Multifactor when request comes from outside an IP range
* @overview Trigger multifactor authentication when IP is outside the expected range.
* @gallery true
diff --git a/src/rules/incognia-authentication.js b/src/rules/incognia-authentication.js
index d3edab19..b8604873 100644
--- a/src/rules/incognia-authentication.js
+++ b/src/rules/incognia-authentication.js
@@ -6,7 +6,7 @@
*
* - `INCOGNIA_CLIENT_ID`: The client ID obtained from Incognia's dashboard (My Apps > API Credentials)
* - `INCOGNIA_CLIENT_SECRET`: The client secret obtained from Incognia's dashboard (My Apps > API Credentials)
- *
+ *
* @title Incognia Authentication Rule
* @overview Verify if the device logging in is at a trusted location.
* @gallery false
diff --git a/src/rules/incognia-onboarding.js b/src/rules/incognia-onboarding.js
index 74335c8b..813d8bc9 100644
--- a/src/rules/incognia-onboarding.js
+++ b/src/rules/incognia-onboarding.js
@@ -6,7 +6,7 @@
*
* - `INCOGNIA_CLIENT_ID`: The client ID obtained from Incognia's dashboard (My Apps > API Credentials)
* - `INCOGNIA_CLIENT_SECRET`: The client secret obtained from Incognia's dashboard (My Apps > API Credentials)
- *
+ *
* @title Incognia Onboarding Rule
* @overview Verify if the device location behavior matches the address declared during onboarding.
* @gallery false
diff --git a/src/rules/ip-address-blocklist.js b/src/rules/ip-address-blocklist.js
index a38882bb..e97625f4 100644
--- a/src/rules/ip-address-blocklist.js
+++ b/src/rules/ip-address-blocklist.js
@@ -1,7 +1,7 @@
/**
*
* This rule will deny access to an app from a specific set of IP addresses.
- *
+ *
* @title IP Address Blocklist
* @overview Do not allow access to an app from a specific set of IP addresses.
* @gallery true
diff --git a/src/rules/migrate-root-attributes.js b/src/rules/migrate-root-attributes.js
index 24af4c37..f80510fb 100644
--- a/src/rules/migrate-root-attributes.js
+++ b/src/rules/migrate-root-attributes.js
@@ -8,7 +8,7 @@
* 1- The rule updates the profile root attribute with the mapped field from user_metadata.
* 2- The mapped fields from user_metadata will be removed following the update.
* 3- This rule will be executed on each login event. For signup scenarios, you should only consider using this rule if you currently use a custom signup form or Authentication Signup API, as these signup methods do not support setting the root attributes.
- *
+ *
* @title Move user metadata attributes to profile root attributes
* @overview Moves select data from user_metadata to profile root attributes (family_name, given_name, name, nickname and picture).
* @gallery true
diff --git a/src/rules/netlify-role-management.js b/src/rules/netlify-role-management.js
index ffd4faa8..4fab85b9 100644
--- a/src/rules/netlify-role-management.js
+++ b/src/rules/netlify-role-management.js
@@ -5,7 +5,7 @@
* - `DEFAULT_ROLE_NAME` - name of the default role to be given to a user
* - `DEFAULT_ROLE_ID` - id of the role to be given to a user
* - `CUSTOM_CLAIMS_NAMESPACE` - namespace for adding custom claims to ID Token
- *
+ *
* @title Netlify Role Management
* @overview Adds a default role if the user doesn't have any yet and attaches roles to the ID Token.
* @gallery true
diff --git a/src/rules/onetrust-consent-management.js b/src/rules/onetrust-consent-management.js
index 8ac40d96..a6595684 100644
--- a/src/rules/onetrust-consent-management.js
+++ b/src/rules/onetrust-consent-management.js
@@ -11,7 +11,7 @@
* **Optional configuration:**
*
* - `ONETRUST_SKIP_IF_NO_EMAIL` If set to "true" then the Rule will be skipped if there is no email address. Otherwise the Rule will fail with an error.
- *
+ *
* @title OneTrust Consent Management
* @overview Enhance Auth0 user profiles with consent, opt-ins and communication preferences data.
* @gallery false
diff --git a/src/rules/scaled-access-relationships-claim.js b/src/rules/scaled-access-relationships-claim.js
index b40bbd54..c104be39 100644
--- a/src/rules/scaled-access-relationships-claim.js
+++ b/src/rules/scaled-access-relationships-claim.js
@@ -13,7 +13,7 @@
* **Optional configuration:**
*
* - `SCALED_ACCESS_CUSTOMCLAIM` A namespaced ID token claim (defaults to `https://scaledaccess.com/relationships`)
- *
+ *
* @title Scaled Access relationship-based claims
* @overview Adds a claim based on the relationships the subject has in Scaled Access
* @gallery false
diff --git a/src/rules/seczetta-get-risk-score.js b/src/rules/seczetta-get-risk-score.js
index 22c88eda..a4b07d18 100644
--- a/src/rules/seczetta-get-risk-score.js
+++ b/src/rules/seczetta-get-risk-score.js
@@ -17,7 +17,7 @@
* **Helpful Hints**
*
* - The SecZetta API documentation is located here: `https://{{SECZETTA_BASE_URL}}/api/v1/`
- *
+ *
* @title SecZetta Get Risk Score
* @overview Grab the risk score from SecZetta to use in the authentication flow
* @gallery false
diff --git a/src/rules/sendgrid.js b/src/rules/sendgrid.js
index 14b0776e..fe2c7b28 100644
--- a/src/rules/sendgrid.js
+++ b/src/rules/sendgrid.js
@@ -5,7 +5,7 @@
* We use a persistent property `SignedUp` to track whether this is the first login or subsequent ones.
*
* In the same way you can use other services like [Amazon SES](http://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html), [Mandrill](https://auth0.com/mandrill) and few others.
- *
+ *
* @title Send emails through SendGrid
* @overview Send an email to an administrator through SendGrind on the first login of a user.
* @gallery true
diff --git a/src/rules/slack.js b/src/rules/slack.js
index b3bffae4..4b96f209 100644
--- a/src/rules/slack.js
+++ b/src/rules/slack.js
@@ -5,7 +5,7 @@
* **Required configuration** (this Rule will be skipped if any of the below are not defined):
*
* - `SLACK_HOOK_URL` URL to the Slack hook to notify.
- *
+ *
* @title Slack Notification on User Signup
* @overview Slack notification on user signup.
* @gallery true
diff --git a/src/rules/username-attribute.js b/src/rules/username-attribute.js
index 303e201a..6c96283d 100644
--- a/src/rules/username-attribute.js
+++ b/src/rules/username-attribute.js
@@ -3,7 +3,7 @@
* This rule will add one attribute to the user's metadata object on when they log in or sign up
*
* This is useful for cases where you want to add the username to an email using liquid syntax.
- *
+ *
* @title Add Username to AppMetadata
* @overview Adds metadata on when an user first signs up or logs in.
* @gallery true
diff --git a/src/rules/vouched-verification.js b/src/rules/vouched-verification.js
index 5a74156a..203bce21 100644
--- a/src/rules/vouched-verification.js
+++ b/src/rules/vouched-verification.js
@@ -12,7 +12,7 @@
* - `VOUCHED_API_URL` Your Vouched API URL; leave blank unless instructed by your Vouched rep
* - `VOUCHED_ID_TOKEN_CLAIM` Set a `https://vouchedid/is_verified` claim in the ID token with results
* - `VOUCHED_VERIFICATION_OPTIONAL` Set to "true" to succeed even if verification fails
- *
+ *
* @title Vouched Verification
* @overview Verify a person's identity using Vouched.
* @gallery true