Conditional MFA
Multi-Factor Authentication (MFA) is the process of adding extra steps into your authentication pipeline to ensure that your users are who they say they are and prevent malicious actors from impersonating legitimate users. The downside of MFA is that it adds extra friction for the user. One way to alleviate this friction is to only require MFA in certain circumstances. This is often called Adaptive MFA, here I'm going to use the term "Conditional MFA" because Auth0 has a paid feature called Adaptive MFA. What I'll be running through here is how to force your users to do an MFA login only under certain circumstances while staying within the Auth0 free tier and trying out Auth0's Actions product which is currently in Beta.
Set up Auth0
If you want to know how to set up Auth0 for MFA using Auth0 Guardian go through the previous blog in this series. The only thing to change from the setup in this previous post is to turn MFA to Never for everyone. Rather than pushing it on all users we will be making a decision at login time about if they need to complete an MFA check.
A side note on Auth0 Actions
Auth0 Actions are a new feature of Auth0 that allow you to further customise the authentication pipeline by adding your own code to be run after each log in event. They're currently in Beta so I thought I would give them a try for creating a conditional authentication experience. The alternative is Rules. Actions come along with a few advantages including a more up to date editor and the ability to include 3rd party packages from npm.
Type Checking
One of the advantages of the new editor for Actions is that it includes types checking. This really helps to make things more discoverable.
Unfortunately there are some rough edges on the type checking. An example of this is that the event.actor.query.scope
property is a union type that the editor reports as not supporting string operations.
Even if the editor is showing errors it wont stop the code from running. There is not compilation step that confirms the code passes type checking before being run in the pipeline.
I expect that these are just rough edges that will get knocked off before they are out of Beta.
Adding actions to login
To add an action into the login flow you need to press the deploy button at the top right of the actions editor. Then navigate to the Flows page via the menu.
In the flows page select Login to go to the flow of actions related to user login.
Then drag your action into the flow.
Finally hit apply and the action will be deployed into the flow.
For future changes to the same action hitting deploy in the actions editor will update the version in the flow.
Debugging
Debugging of Rules or Actions can be difficult, but there are a couple of built in ways that can be used to support your development workflow.
Test events
Actions have test events that can be run through the pipeline to ensure correctness.
The output will return the object returned, some stats about the execution, and any console.log
output.
{
"payload": {
"output": {
"command": {
"provider": "guardian",
"type": "multifactor"
}
},
"logs": "Sample log #1\nip address 13.33.86.47 is concerning better do an MFA check.\n",
"stats": {
"action_duration_ms": 46,
"boot_duration_ms": 60,
"network_duration_ms": 47
}
}
}
Logging production data
In production as members log some data about the actions that were run are included in a new Actions tab in the logs view. Before relying on this for your debugging workflows you need to be aware that the logs are truncated. It's not clear if this is a feature or a bug in the beta. At the moment I'm not able to work out what the trigger is for log truncation.
Real-time Webtask Logs
The Real-time webtask Logs extension can also be used to debug actions. This extension gives you a temporary view into what is happening in your Actions (or Rules). It's a good tool when there is a low amount of login activity. As the amount of activity goes up the level of noise quickly makes this method of little use.
Building the Action
Enough about what actions are, how can we use them to ensure only some of the users need to do MFA?
For this example lets make an assumption that the CTO of our organisation has a real paranoia of the number 13. They're convinced that anyone logging in from an IP Address that has a the number 13
in it anywhere may have had their account compromised and we need to force them to do an MFA check. Let's have a look at how do to that with Auth0 Actions.
Parse the input
The action provides us with two properties, the event
and the context
.
The event
object has the users IP Address available under the actor
property.
const ipAddress = event.actor.ip;
We can then use that to check if it includes a 13
any where.
if (ipAddress.inclues("13")) {
// Force MFA
console.log(`IP Address ${ipAddress} is concerning better do an MFA check.`);
}
// otherwise continue
console.log("This person is obviously trustworthy let them in.");
Require MFA
In order to require MFA we need to return an object with a command
property which instructs Auth0 that before this user can log in they need to do a MFA check.
To do that add this into the positive case of the if block.
return {
command: {
type: "multifactor",
provider: "guardian",
},
};
If they don't need to do a MFA check we can just return an empty object and the Actions engine will move on to the next action in the flow.
Final action code
My final action code looks like this:
module.exports = async (event, context) => {
console.log("Sample Log #1");
const ipAddress = event.actor.ip;
if (ipAddress.includes("13")) {
console.log(
`ip address ${ipAddress} is concerning better do an MFA check.`
);
return {
command: {
type: "multifactor",
provider: "guardian",
},
};
}
console.log("This person is obviously trustworthy let them in.");
return {};
};
With that, when a user attempts to log in from an IP Address with "13"
in it they will be required to prove who they are using Auth0 Guardian.
Enrolling MFA
A point to note here is that the first time they are required to do an MFA login they will be asked to enrol with Guardian. If you want to force the user to enrol on first login you will need to add some extra code into the action.
Create a new custom action to handle forcing a new user into the MFA flow. In an action we can find out the number of logins done by a user by checking the event.stats.loginsCount
. The 2nd action needs to check this property and return a guardian
mfa command if it is the first login event.stats.loginsCount === 1
.
module.exports = async (event, context) => {
if (event.stats.loginsCount === 1) {
return {
command: {
type: "multifactor",
provider: "guardian",
},
};
}
return {};
};
With this every new sign up will be redirected to set up MFA. This means that if in the future they log in from an ip address that includes 13
then the user can prove who they are using Guardian MFA.
Wrap up
Actions provide a simple way to extend the Authentication pipeline. In particular here we can see that they can quickly be used to create a conditional MFA requirement for some users. The example of requiring people with 13
in their ip address is obviously a bit of an over simplification. The actor
property on the event
parameter also has information on geo-location of the ip address which could be used with impossible travel algorithms to check that your user isn't being impersonated by a malicious actor on the other side of the world. That's probably a better condition for MFA.
Cover Photo by Jen Theodore Unsplash
Top comments (2)
The code you have shown is for an auth0 rule, not an action. The second argument to an auth0 action is "api", not context.
Hi Adam. This was certainly code for actions as they existed in Feb of 21, at this time Acrions was in beta. I'm May of 21 Actions was released to GA and the API changed. Thanks for alerting me to the fact that this post is out of date.