Adaptive MFA
Risk-based multi-factor authentication with per-tenant policy, 18 assessors, and NIST-aligned step-up
Adaptive MFA triggers multi-factor authentication only when a login looks risky — not on every sign-in. Wocha combines a real-time risk engine, configurable MFA policies, device trust, and NIST-aligned factor prioritisation into a single consent-time pipeline. Unlike platforms that hide risk signals internally, Wocha exposes numeric scores, per-assessor confidence, and full audit history to your team and your Auth Actions code.
Overview
Traditional MFA is binary: every user completes a second factor on every login, or MFA is off entirely. Adaptive MFA sits between those extremes. Wocha scores each OAuth consent request against behavioural, geographic, device, and network signals. When the combined score crosses your threshold, the user is stepped up to AAL2 (or AAL3 for hardware keys). Low-risk logins proceed without friction.
What makes Wocha's implementation industry-leading:
- Transparent scoring — numeric risk score (0.0–1.0) and per-assessor breakdown, not a black box
- 18 consent-time assessors — bot detection (weighted sub-scoring), velocity, geo-velocity, impossible travel (city-level), device trust, device anomaly, anonymous proxy/VPN, multi-device velocity, concurrent geo sessions, account age risk, IP reputation, datacenter ASN, time-of-day baselines, phone risk, SIM-swap, SMS abuse, and more
- Adaptive challenge ladder — graduated responses from invisible CAPTCHA through Proof-of-Work to MFA step-up, integrated with bot protection
- Per-tenant and per-org policy —
never,adaptive, oralwaysMFA with org-level overrides - NIST-aligned ACR/AMR —
urn:wocha:aal1/aal2/aal3with RFC 8176 authentication method references - Configurable device trust TTL — known devices expire after a configurable period (default 30 days)
- Shadow mode — score and log without enforcing, so you can tune thresholds on real traffic
- Self-hosted parity — full adaptive MFA stack available on-premises
Architecture
At consent time (when a user approves an OAuth authorisation), Wocha runs a unified pipeline:
The risk engine, MFA policy engine, device trust store, and factor prioritiser work together:
| Component | Role |
|---|---|
Risk engine (@wocha/risk-engine) | Scores the login context; returns verdict (allow, step_up, block) and per-assessor details |
| MFA policy | Decides whether a step_up verdict (or always policy) triggers MFA enforcement |
| Device trust | Fingerprints stored in Postgres; devices older than the trust TTL are treated as unknown |
| Factor prioritisation | When MFA is required, challenges the strongest enrolled factor (WebAuthn > TOTP > SMS > Email) |
| ACR/AMR | Maps the completed session to NIST-aligned token claims |
Evaluation runs under a 150ms hard deadline. Rules execute cheapest-first with early exit when the block threshold is reached.
MFA policy model
Each tenant stores an MFA policy in tenant_configs under the key mfa_policy. Three policy levels are available:
| Policy | Behaviour |
|---|---|
never | MFA is never enforced by the risk engine or tenant policy. Org-level required enforcement still applies. |
adaptive | MFA is enforced when the risk verdict is step_up. Threshold strictness is controlled by adaptiveThreshold. |
always | Every first-party consent requires MFA (enrolled users step up; unenrolled users follow unenrolledUserAction). |
Additional policy fields:
| Field | Default | Description |
|---|---|---|
gracePeriodMs | 24 hours | New accounts can skip MFA enforcement during this window |
deviceTrustTtlDays | 30 | Days before a known device fingerprint is treated as unknown |
adaptiveThreshold | medium | Maps to risk step-up threshold: low → 0.2, medium → 0.3, high → 0.5 |
allowedFactors | totp, webauthn | Which MFA factors can be challenged (add sms or email to enable) |
preferredFactor | — | Override NIST priority when this factor is enrolled |
ignoreRememberDeviceOnRisk | true | Ignore "remember device" when risk triggers step-up |
unenrolledUserAction | force_enrollment | email_challenge, force_enrollment, or allow when MFA is required but the user has no factors |
Risk assessors
Wocha evaluates 18 assessors at consent time, grouped by category. Each has a default weight (contribution to the aggregate score when triggered):
Bot detection
| Assessor | Default weight | What it detects |
|---|---|---|
bot_detected | 0.8 | Weighted sub-scoring across 13 client SDK signals — WebDriver, automation globals, headless browser, canvas/WebGL/audio fingerprint anomalies, timing, plugins, UA, screen, timezone/language mismatch. Triggers at composite sub-score >= 0.5 with confidence >= 0.3. |
Network and geo
| Assessor | Default weight | What it detects |
|---|---|---|
ip_reputation | 0.6 | Source IP on the deny-list (abuse, anonymiser, Tor, datacenter, reputation) |
impossible_travel | 0.5 | Login from a location requiring travel faster than 900 km/h. Uses city-level lat/lon coordinates (MaxMind GeoLite2-City) with fallback to 200+ country centroids. Skips when VPN/proxy detected (reduces false positives). |
concurrent_geo_sessions | 0.4 | Active sessions from 2+ countries simultaneously — a strong account takeover indicator |
anonymous_proxy | 0.35 | Login from anonymous proxy, VPN exit node, or Tor relay (MaxMind Anonymous IP DB) |
geo_anomaly | 0.3 | Login from an unusual geographic region for this user |
datacenter_ip | 0.25 | Source IP resolves to a known cloud provider ASN (AWS, GCP, Azure, etc.) |
new_country | 0.15 | First login from this country for the user |
Device intelligence
| Assessor | Default weight | What it detects |
|---|---|---|
device_anomaly | 0.35 | Device flagged as suspicious, or fingerprint component drift (2+ hardware properties changed with the same hash, suggesting spoofing) |
multi_device_anomaly | 0.3 | 5+ distinct devices used within 24 hours for the same user |
unknown_device | 0.2 | First-time device fingerprint, suspicious device, or device past trust TTL |
Behavioural
| Assessor | Default weight | What it detects |
|---|---|---|
credential_stuffing | 0.6 | High rate of failed login attempts for the user or IP (8+ in 1 hour) |
velocity_anomaly | 0.4 | Unusual login frequency (5+ logins in 5 min, 20+ in 1 hour, 10+ token issuances in 5 min) |
account_age_risk | 0.15 | Account created within the last 24 hours |
time_pattern_anomaly | 0.1 | Login hour falls outside the user's 90-day behavioural baseline (< 5% of historical logins) |
Phone and SMS
| Assessor | Default weight | What it detects |
|---|---|---|
sim_swap_detected | 0.5 | SIM card swapped within the last 7 days (Twilio Lookup or context signal) |
sms_abuse | 0.4 | Excessive SMS OTP requests to a phone number (10+ in 1 hour) |
phone_risk | 0.3 | VoIP/virtual number, country mismatch with geo-IP, or shared number across accounts |
Per-rule weights, enable/disable toggles, and thresholds are configurable per tenant in the Console or via the Customer API. Rules are evaluated cheapest-first with a 150ms hard deadline and early exit when the block threshold is reached.
Phone-related risk signals
When a user has a phone number on their identity or is enrolling SMS MFA, three assessors evaluate phone-specific risk:
| Signal | Assessor | Details |
|---|---|---|
| VoIP detection | phone_risk | Flags virtual/VoIP numbers via prefix list or Twilio Lookup is_voip signal |
| Country mismatch | phone_risk | Phone country code does not match geo-IP country of the login |
| Shared number | phone_risk | Same E.164 number registered on multiple identities |
| SIM swap | sim_swap_detected | SIM card swapped within 7 days (Twilio Lookup v2; advisory, not blocking by default) |
| SMS OTP velocity | sms_abuse | More than 10 OTP send requests to a phone in 1 hour |
SIM-swap detection requires Twilio credentials configured in SMS risk settings
(sim_swap_check: true). When the lookup store is unavailable, the assessor skips gracefully
without affecting other rules.
SMS step-up challenges the user's enrolled phone via the Wocha SMS Gateway. When sms is
the preferred or only enrolled factor, the auth app redirects to /sms-verify instead of
native Kratos AAL2. See the SMS MFA API reference for the step-up flow.
Confidence model
Each assessor returns an ordinal confidence level alongside the numeric weight:
| Assessor state | Confidence | Meaning |
|---|---|---|
| Not triggered | high | This signal does not indicate risk |
| Triggered, weight ≥ 0.5 | low | Strong risk signal |
| Triggered, weight ≥ 0.2 | medium | Moderate risk signal |
| Not evaluated (budget exhausted) | neutral | Assessor did not run in time |
The overall assessment also maps the verdict to confidence: allow → high, step_up → medium, block → low.
Auth Actions receive the full riskAssessment object:
Configuration
Console UI
Navigate to Settings → Security → MFA Policy to configure:
- Policy selector (
Never/Adaptive/Always) - Grace period and device trust TTL sliders
- Adaptive threshold (
Low (strict)/Medium/High (lenient)) - Allowed factors checkboxes (TOTP, WebAuthn, SMS, Email)
- Preferred factor dropdown
- Unenrolled user action
- "Ignore remember device on risk" toggle
Navigate to Settings → Risk & Fraud for risk engine settings:
- Master enable/disable and shadow mode
- Step-up and block thresholds
- Per-rule weight sliders
- IP allowlist (CIDR ranges)
The Security → Risk Dashboard shows per-assessor breakdowns, confidence distribution, MFA trigger rate, and top triggered rules.
Customer API
Shadow mode
Enable shadow mode before turning on enforcement. The engine scores and logs every consent decision without blocking or stepping up. Review events in the Console risk dashboard or via GET /v1/customer/risk/events, then tune thresholds before disabling shadow mode.
Per-Organisation Policies
Organisations inherit the tenant MFA policy by default and can override specific fields to be stricter. See the full Organisation security policies guide for password and session policies.
MFA enforcement levels
Set mfaEnforcement per organisation (Console Security tab or Customer API):
| Value | Behaviour |
|---|---|
optional | MFA only when tenant policy, adaptive risk, or org mfaPolicy override requires it |
recommended | Unenrolled users see an interstitial (/mfa-recommended) with "Set up now" and "Skip for now"; dismissal suppresses the prompt for 7 days. Enrolled users at AAL1 are stepped up. |
required | Always require MFA for members of this org — no grace period |
Org-level required enforcement runs after tenant policy and can be stricter than a never tenant policy.
MFA policy override
Orgs can set their own mfaPolicy (never / adaptive / always) independently of enforcement level:
mfaEnforcement— who must have MFA enrolledmfaPolicy— when to challenge at consent time
Example: tenant adaptive + org always + org required → all org members always face MFA, regardless of risk score.
Per-org factor configuration
When mfaEnforcement is optional, orgs can still override allowedFactors and preferredFactor. Allowed factors must be a subset of the tenant's allowed factors. These overrides apply when adaptive MFA triggers step-up for that org's members.
ACR and AMR
Wocha issues NIST-aligned Authentication Context Class Reference (ACR) and Authentication Methods References (AMR) claims in ID and access tokens.
| ACR URN | Assurance level | Typical factors |
|---|---|---|
urn:wocha:aal1 | Single factor | Password, passkey, or social login |
urn:wocha:aal2 | Two factors | Above + TOTP, SMS, or email OTP |
urn:wocha:aal3 | Hardware-bound | Above + hardware WebAuthn security key |
AMR values follow RFC 8176:
| AMR | Method |
|---|---|
pwd | Password |
otp | TOTP or email OTP |
sms | SMS OTP |
hwk | Hardware WebAuthn key |
swk | Software passkey |
fed | Federated (OIDC/social) |
mfa | Composite — two or more distinct factors |
Request step-up by passing acr_values in the OAuth authorise request. See the Step-up authentication guide for code examples.
Auth Actions
Pre-token Auth Actions run after risk scoring and before MFA enforcement. Use riskAssessment to make custom decisions and challengeMfa() to force a step-up:
challengeMfa(factor?) sets a flag the consent processor reads. When called, the user is redirected to AAL2 step-up even if the risk verdict was allow.
SDK helpers
Check ACR levels in your application middleware with helpers from @wocha/nextjs and @wocha/express:
| Helper | Returns true when |
|---|---|
requireAal2(token) | acr is urn:wocha:aal2, urn:wocha:aal3, or legacy aal2 |
requireAal3(token) | acr is urn:wocha:aal3 or legacy aal3 |
Related
- Organisation security policies — MFA, password, and session policies per org
- Organisation security policy API — Customer API reference
- Step-up authentication — request specific ACR levels in OAuth flows
- MFA API reference — TOTP enrolment and risk events endpoints
- SMS MFA API reference — SMS MFA enrolment and step-up challenges
- SMS setup guide — configure SMS providers in the Console
- Passkeys guide — WebAuthn enrolment and AAL3
- Security overview — platform security posture