From 5009fb16647bcd82df4760290b07b0d61955dac6 Mon Sep 17 00:00:00 2001 From: Fady Makram Date: Thu, 28 Feb 2019 10:11:40 +0100 Subject: [PATCH] [IPS-169] Add authentication object to Rule's context (#199) * [IPS-169] Add authentication object to Rule's context * Rename rule * 0.8.0 * Build and bump version --- package-lock.json | 2 +- package.json | 2 +- rules.json | 12 +++- src/rules/require-mfa-once-per-session.js | 28 +++++++++ test/rules/require-mfa-once-per-session.js | 68 ++++++++++++++++++++++ test/utils/authenticationBuilder.js | 23 ++++++++ test/utils/contextBuilder.js | 7 ++- 7 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 src/rules/require-mfa-once-per-session.js create mode 100644 test/rules/require-mfa-once-per-session.js create mode 100644 test/utils/authenticationBuilder.js diff --git a/package-lock.json b/package-lock.json index 34418078..adff93f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rules-templates", - "version": "0.7.4", + "version": "0.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 95a15574..b047ad9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rules-templates", - "version": "0.7.4", + "version": "0.8.0", "description": "Auth0 Rules Repository", "main": "./rules", "scripts": { diff --git a/rules.json b/rules.json index 38e1e7cf..8309082d 100644 --- a/rules.json +++ b/rules.json @@ -120,7 +120,7 @@ "access control" ], "description": "

This rule adds a Roles field to the user based on some pattern.

", - "code": "function (user, context, callback) {\n\n // Roles should only be set to verified users.\n if (!user.email || !user.email_verified) {\n return callback(null, user, context);\n }\n\n user.app_metadata = user.app_metadata || {};\n // You can add a Role based on what you want\n // In this case I check domain\n const addRolesToUser = function(user) {\n const endsWith = '@example.com';\n\n if (user.email && (user.email.substring(user.email.length - endsWith.length, user.email.length) === endsWith)) {\n return ['admin']\n }\n return ['user'];\n };\n\n const roles = addRolesToUser(user);\n\n user.app_metadata.roles = roles;\n auth0.users.updateAppMetadata(user.user_id, user.app_metadata)\n .then(function() {\n context.idToken['https://example.com/roles'] = user.app_metadata.roles;\n callback(null, user, context);\n })\n .catch(function (err) {\n callback(err);\n });\n}" + "code": "function (user, context, callback) {\n\n // Roles should only be set to verified users.\n if (!user.email || !user.email_verified) {\n return callback(null, user, context);\n }\n\n user.app_metadata = user.app_metadata || {};\n // You can add a Role based on what you want\n // In this case I check domain\n const addRolesToUser = function (user) {\n const endsWith = '@example.com';\n\n if (user.email && (user.email.substring(user.email.length - endsWith.length, user.email.length) === endsWith)) {\n return ['admin'];\n }\n return ['user'];\n };\n\n const roles = addRolesToUser(user);\n\n user.app_metadata.roles = roles;\n auth0.users.updateAppMetadata(user.user_id, user.app_metadata)\n .then(function () {\n context.idToken['https://example.com/roles'] = user.app_metadata.roles;\n callback(null, user, context);\n })\n .catch(function (err) {\n callback(err);\n });\n}" }, { "id": "simple-domain-whitelist", @@ -520,6 +520,16 @@ ], "description": "

This rule is used to trigger multifactor authentication when a condition is met.

", "code": "function (user, context, callback) {\n /*\n You can trigger MFA conditionally by checking:\n 1. Client ID:\n context.clientID === 'REPLACE_WITH_YOUR_CLIENT_ID'\n 2. User metadata:\n user.user_metadata.use_mfa\n */\n\n // if () {\n context.multifactor = {\n provider: 'any',\n\n // optional, defaults to true. Set to false to force authentication every time.\n // See https://auth0.com/docs/multifactor-authentication/custom#change-the-frequency-of-authentication-requests for details\n allowRememberBrowser: false\n };\n //}\n\n callback(null, user, context);\n}" + }, + { + "id": "require-mfa-once-per-session", + "title": "Require MFA once per session", + "overview": "Require multifactor authentication only once per session", + "categories": [ + "multifactor" + ], + "description": "

This rule can be used to avoid prompting a user for multifactor authentication if they have successfully completed MFA in their current session.

\n

This is particularly useful when performing silent authentication (prompt=none) to renew short-lived access tokens in a SPA (Single Page Application) during the duration of a user's session without having to rely on setting allowRememberBrowser to true.

", + "code": "function (user, context, callback) {\n const completedMfa = !!context.authentication.methods.find(\n (method) => method.name === 'mfa'\n );\n \n if (completedMfa) {\n return callback(null, user, context);\n }\n \n context.multifactor = {\n provider: 'any',\n allowRememberBrowser: false\n };\n \n callback(null, user, context);\n}" } ] }, diff --git a/src/rules/require-mfa-once-per-session.js b/src/rules/require-mfa-once-per-session.js new file mode 100644 index 00000000..da072196 --- /dev/null +++ b/src/rules/require-mfa-once-per-session.js @@ -0,0 +1,28 @@ +/** + * @title Require MFA once per session + * @overview Require multifactor authentication only once per session + * @gallery true + * @category multifactor + * + * This rule can be used to avoid prompting a user for multifactor authentication if they have successfully completed MFA in their current session. + * + * This is particularly useful when performing silent authentication (`prompt=none`) to renew short-lived access tokens in a SPA (Single Page Application) during the duration of a user's session without having to rely on setting `allowRememberBrowser` to `true`. + * + */ + +function (user, context, callback) { + const completedMfa = !!context.authentication.methods.find( + (method) => method.name === 'mfa' + ); + + if (completedMfa) { + return callback(null, user, context); + } + + context.multifactor = { + provider: 'any', + allowRememberBrowser: false + }; + + callback(null, user, context); +} diff --git a/test/rules/require-mfa-once-per-session.js b/test/rules/require-mfa-once-per-session.js new file mode 100644 index 00000000..ab887dab --- /dev/null +++ b/test/rules/require-mfa-once-per-session.js @@ -0,0 +1,68 @@ +'use strict'; + +const loadRule = require('../utils/load-rule'); +const ContextBuilder = require('../utils/contextBuilder'); +const RequestBuilder = require('../utils/requestBuilder'); +const AuthenticationBuilder = require('../utils/authenticationBuilder'); + +const ruleName = 'require-mfa-once-per-session'; + +describe(ruleName, () => { + let context; + let rule; + let user; + + describe('With only a login prompt completed', () => { + beforeEach(() => { + rule = loadRule(ruleName); + + const request = new RequestBuilder().build(); + const authentication = new AuthenticationBuilder().build(); + context = new ContextBuilder() + .withRequest(request) + .withAuthentication(authentication) + .build(); + }); + + it('should set a multifactor provider', (done) => { + rule(user, context, (err, u, c) => { + expect(c.multifactor.provider).toBe('any'); + expect(c.multifactor.allowRememberBrowser).toBe(false); + + done(); + }); + }); + }); + + describe('With a login and MFA prompt completed', () => { + beforeEach(() => { + rule = loadRule(ruleName); + + const request = new RequestBuilder().build(); + const authentication = new AuthenticationBuilder() + .withMethods([ + { + name: 'pwd', + timestamp: 1434454643024 + }, + { + name: 'mfa', + timestamp: 1534454643881 + } + ]) + .build(); + context = new ContextBuilder() + .withRequest(request) + .withAuthentication(authentication) + .build(); + }); + + it('should not set a multifactor provider', (done) => { + rule(user, context, (err, u, c) => { + expect(c.multifactor).toBe(undefined); + + done(); + }); + }); + }); +}); diff --git a/test/utils/authenticationBuilder.js b/test/utils/authenticationBuilder.js new file mode 100644 index 00000000..9164e4df --- /dev/null +++ b/test/utils/authenticationBuilder.js @@ -0,0 +1,23 @@ +'use strict'; + +class AuthenticationBuilder { + constructor() { + this.authentication = { + methods: [ + { + name: 'pwd', + timestamp: 1434454643024 + } + ] + } + } + withMethods(methods) { + this.authentication.methods = methods; + return this; + } + build() { + return this.authentication; + } +} + +module.exports = AuthenticationBuilder; diff --git a/test/utils/contextBuilder.js b/test/utils/contextBuilder.js index c2de4d19..bd9d8469 100644 --- a/test/utils/contextBuilder.js +++ b/test/utils/contextBuilder.js @@ -23,7 +23,8 @@ class ContextBuilder { accessToken: {}, idToken: {}, sessionID: 'jYA5wG...BNT5Bak', - request: {} + request: {}, + authentication: {} }; this.context.request = new RequestBuilder().build(); } @@ -67,6 +68,10 @@ class ContextBuilder { this.context.stats = stats; return this; } + withAuthentication(authentication) { + this.context.authentication = authentication; + return this; + } build() { return this.context; }