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.
When to use it
Section titled “When to use it”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.
What you'll build
Section titled “What you'll build”A Login flow where the Post-Auth stage runs three blocks in sequence:
- Risk Evaluate — aggregates all active signals (IP reputation, brute force history, device fingerprint, breached-credential check) into a single recommendation and numeric score.
- Decision — reads the recommendation and branches:
challengegoes to MFA;allowgoes directly to Issue Session. - 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 SessionNo custom action is needed. This is pure block composition.
Configure it
Section titled “Configure it”Step 1 — Open the Login flow editor
Section titled “Step 1 — Open the Login flow editor”Go to Flows in the left navigation, click Login, then click Edit. The canvas opens with your existing Pre-Login and Post-Login stages.
Step 2 — Add Risk Evaluate
Section titled “Step 2 — Add Risk Evaluate”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, ordenystep.<slug>.score— a numeric score from 0 to 100step.<slug>.severity—none,low,medium,high, orcriticalstep.<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.
Step 3 — Add a Decision block
Section titled “Step 3 — Add a Decision block”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 label | When expression | Route to |
|---|---|---|
step-up | step.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.
Step 6 — Save and publish
Section titled “Step 6 — Save and publish”Click Save, review the diff, and click Publish. The new behavior takes effect on the next login.
Test it
Section titled “Test it”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}Verify on a live flow
Section titled “Verify on a live flow”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.
This recipe does not write any state outside the flow run. The Test pane and real logins are both safe to exercise against production identities.
Cautions
Section titled “Cautions”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.