Skip to main content

Rules for MFA on sign-in

This guide shows how to use rules to challenge users at sign-in only if the following conditions are met:

  1. The request is from a new device which has not previously been authenticated
  2. The request appears to be made by a bot based on the user agent
  3. The request is anonymous or originates from behind a proxy server

As our starting point we'll adapt the sign-in example using Authsignal with Supabase which presents users with an MFA challenge every time they sign in. This guide will show the changes required to instead present users with an MFA challenge at sign-in based on the criteria above.

The only code changes required will be to send some additional fields when calling track:

  1. deviceId - we'll import the Authsignal browser client and use the anonymousId value to identify the device
  2. ipAddress - we'll get this from the request headers via the request-ip Node.js package
  3. userAgent - we'll get this from the request headers directly

You can find a diff of the code changes required on Github.

Step 1: Configuring rules

The first step is to configure rules for the signIn action which we created in the previous guide. In the Authsignal Portal go to the rules for the signIn action and click Create rule.

Rules section in an empty state with a prompt to create a new rule

Enter a name for the rule - for example, Challenge "high risk" users at sign-in - and enter a description if you like, or leave it blank.

Rule name and descriptions

Next set the rule outcome to Challenge.

Rule outcome set to challenge

Then click Add feature below and click on Select feature. Choose the Device category and select the Device is new feature. Repeat this process for the Device is a bot feature (in the Device category) and for the IP is anonymous feature (in the IP/Network category).

Selecting a feature

Now change the conjunction logic from AND to OR so that the rule will be triggered if any of the conditions are met.

You should now see three conditions for your new rule.

Rule conditions

At the bottom of the page click Save rule. You will be returned to the Rules page where the rule you just created will appear.

Newly created rule displayed in the Rules section

Finally, change the default outcome of the signIn action to ALLOW and click Save. This means the default behaviour when signing in will be to allow users through without an MFA challenge if they don't meet any of the rule conditions.

Default action outcome - allow

Step 2: Installing additional dependencies

Install the @authsignal/nextjs-helpers package and the request-ip package:

npm install @authsignal/nextjs-helpers request-ip
npm install --save-dev @types/request-ip

Step 3: Adding the publishable key env var

To use the Authsignal browser client, we need to update the .env.local file to add the Authsignal publishable key. This value can be found in the Authsignal Portal -> Settings -> Api Keys page. Use the NEXT_PUBLIC_ prefix so it is available to the browser.

NEXT_PUBLIC_AUTHSIGNAL_PUBLISHABLE_KEY=get-this-value-from-authsignal-portal

Step 4: Getting the deviceId client-side

To identify a user's device, we can use the anonymousId provided by the Authsignal browser client as the deviceId. We'll use the @authsignal/nextjs-helpers package to make this browser client available via React context.

Change the code in _app.tsx to import and use the AuthsignalProvider component:

import { AuthsignalProvider } from "@authsignal/nextjs-helpers";
import type { AppProps } from "next/app";
import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthsignalProvider>
<Component {...pageProps} />
</AuthsignalProvider>
);
}

export default MyApp;

Then change both the index.tsx and sign-in.tsx page components to pass the deviceId to the api routes. For each file:

  1. Import the useAuthsignal hook:
import { useAuthsignal } from "@authsignal/nextjs-helpers";
  1. Get the anonymousId and rename it as deviceId:
const { anonymousId: deviceId } = useAuthsignal();
  1. Add the deviceId to the fetch request body:

body: JSON.stringify({ isEnrolled, deviceId }),

Step 5: Updating the server-side track calls

Update the pages/api/sign-in.ts api route to send the additional fields with track.

  1. Import the request-ip library:
import requestIp from "request-ip";
  1. Send the additional fields with track
const { state, url: mfaUrl } = await authsignal.track({
action: "signIn",
userId: data.user.id,
deviceId: req.body.deviceId,
userAgent: req.headers["user-agent"],
ipAddress: requestIp.getClientIp(req) ?? undefined,
});

Lastly update the pages/api/mfa.ts api route to send just the deviceId. We do this to track the device which the user is on when they enroll for the first time.

const { url: mfaUrl } = await authsignal.track({
action: isEnrolled ? "manageSettings" : "enroll",
userId: user.id,
redirectToSettings: isEnrolled,
deviceId: req.body.deviceId,
});

That's it! With this additional request data provided to track, and with a rule configured in the Authsignal Portal, users will now be prompted to complete an MFA challenge on sign-in only if they meet certain conditions which we've designated as "high risk".