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:
- The request is from a new device which has not previously been authenticated
- The request appears to be made by a bot based on the user agent
- 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
:
deviceId
- we'll import the Authsignal browser client and use theanonymousId
value to identify the deviceipAddress
- we'll get this from the request headers via the request-ip Node.js packageuserAgent
- 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.
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.
Next set the rule outcome 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).
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.
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.
Finally, change the default outcome of the signIn
action to ALLOW
and click Save. This means the default behavior when signing in will be to allow users through without an MFA challenge if they don't meet any of the rule conditions.
Step 2: Installing additional dependencies
Install the @authsignal/nextjs-helpers
package and the request-ip
package:
- npm
- Yarn
- pnpm
npm install @authsignal/nextjs-helpers request-ip
npm install --save-dev @types/request-ip
yarn add @authsignal/nextjs-helpers request-ip
yarn add --dev @types/request-ip
pnpm add @authsignal/nextjs-helpers request-ip
pnpm add --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:
- Import the
useAuthsignal
hook:
import { useAuthsignal } from "@authsignal/nextjs-helpers";
- Get the
anonymousId
and rename it asdeviceId
:
const { anonymousId: deviceId } = useAuthsignal();
- 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
.
- Import the
request-ip
library:
import requestIp from "request-ip";
- 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".