Skip to content

Recipe: Step-up MFA on risk score

Add friction only when you need it. This recipe wires the Risk Evaluate block into your Login flow and routes the result to a Decision block that either demands a second factor or lets the user straight through — depending entirely on what the risk engine found.

Require MFA when the platform's risk evaluation scores a login as suspicious. Leave low-risk logins untouched. Users on trusted networks from familiar devices never see an MFA prompt; users arriving through Tor, a flagged VPN, or with brute-force history get challenged before a session is issued.

This pattern fits B2B and B2C tenants alike. It gives you a better security posture than "always require MFA" and a better user experience than "never require MFA." Cymmetri, for example, uses it on their customer portal: 97% of logins complete without an extra step, and the 3% that are challenged are exactly the ones worth challenging.

Reach for a harder policy — like the Deny Tor and VPN logins recipe — only when your compliance requirements prohibit anonymous-network traffic outright.

A Login flow where the Post-Auth stage runs three blocks in sequence:

  1. Risk Evaluate — aggregates all active signals (IP reputation, brute force history, device fingerprint, breached-credential check) into a single recommendation and numeric score.
  2. Decision — reads the recommendation and branches: challenge goes to MFA; allow goes directly to Issue Session.
  3. MFA — on the challenge branch only. After a successful second-factor verification, the flow continues to Issue Session at AAL2.
Login flow
Stage: Pre-Login
Block: Identity Lookup
Block: Password Check
Stage: Post-Auth
Block: Risk Evaluate <-- new
Block: Decision <-- new
Block: MFA <-- new (challenge branch only)
Stage: Post-Login
Block: Issue Session

No custom action is needed. This is pure block composition.

Go to Flows in the left navigation, click Login, then click Edit. The canvas opens with your existing Pre-Login and Post-Login stages.

In the Post-Auth stage, open the block picker and drag Risk Evaluate onto the canvas. You do not need to change any default settings for this recipe. The block reads event.request.ip, event.request.user_agent, event.request.device_fp, and event.user.id automatically at runtime.

When the block runs it writes:

  • step.<slug>.recommendation — the engine's verdict: allow, challenge, or deny
  • step.<slug>.score — a numeric score from 0 to 100
  • step.<slug>.severitynone, low, medium, high, or critical
  • step.<slug>.reasons — the signal IDs that contributed, e.g. ["vpn", "brute_force"]

The slug defaults to risk-evaluate. If you rename it, use your slug in the Decision expression below.

Drag a Decision block immediately below Risk Evaluate in the same Post-Auth stage.

Step 4 — Configure the Decision branches

Section titled “Step 4 — Configure the Decision branches”

Open the Decision block's settings panel. Add two outbound branches:

Branch labelWhen expressionRoute to
step-upstep.risk-evaluate.recommendation == "challenge"MFA block (below)
proceed(default)Issue Session

The default branch catches both allow and deny recommendations. In most tenants you want the deny recommendation to route to Issue Session as a fail-open, letting the user through with an AAL1 session. If your policy is stricter — deny on deny, challenge on challenge — add a third branch for step.risk-evaluate.recommendation == "deny" that routes to a Deny terminal block.

Step 5 — Add MFA on the challenge branch

Section titled “Step 5 — Add MFA on the challenge branch”

On the step-up branch that flows out of the Decision block, drag an MFA block. Leave the default factor setting unless your tenant has a specific factor preference. After MFA succeeds the flow continues to the existing Issue Session block, and the session is issued at AAL2.

Click Save, review the diff, and click Publish. The new behavior takes effect on the next login.

Use the Test panel built into the Decision block (click the beaker icon on the block header) to exercise the two branches without a real login.

For a more complete test, use the Simulate endpoint from your browser's developer console or a tool like curl. Two payloads cover the branches:

Low-risk payload — should route to Issue Session:

// Simulate a clean request: domestic IP, no VPN, no brute force history
// Expected: step.risk-evaluate.recommendation == "allow"
// Decision routes to "proceed" branch
// No MFA prompt
{
"event.request.geo.country": "US",
"event.request.asn.is_vpn": false,
"event.request.asn.is_tor": false
}

High-risk payload — should trigger the step-up branch:

// Simulate a Tor exit node: risk engine scores high
// Expected: step.risk-evaluate.recommendation == "challenge"
// Decision routes to "step-up" branch
// MFA prompt shown
{
"event.request.asn.is_tor": true
}

After publishing, sign in to your tenant from a VPN connection. You should see the MFA prompt appear after entering your password. Then sign in from your regular network without a VPN — no MFA prompt should appear.

Tune the threshold to your tolerance. The risk engine's default thresholds are tuned for general-purpose CIAM. A score above 70 produces challenge out of the box. If your tenant serves high-risk transactions, lower the threshold in Risk Evaluate's settings. If your tenant accepts more friction to reduce false challenges, raise it.

Confirm your users can actually complete MFA. The MFA block fails if the user has no enrolled factor. Combine this recipe with the require_mfa_enroll block (see Authentication blocks reference) if you want users who have never enrolled to be prompted for enrollment the first time they are challenged, rather than seeing an error.

The risk engine fails open. If an individual signal source is temporarily unreachable — for example, if the IP reputation service cannot be contacted — that signal contributes zero to the score rather than halting the flow. Check step.risk-evaluate.signal_results in the audit log if you need to know which signals fired on a given login.

The deny recommendation is not an automatic block. Risk Evaluate writes a recommendation; it never terminates a flow on its own. If your policy requires denying logins that the engine marks as deny, add a third Decision branch that routes to a Deny terminal.