Adaptive authentication is the pattern where the platform asks for more proof when a sign-in looks suspicious, and asks for less when it looks routine. Instead of "always require MFA" or "never require MFA", the rule is "require MFA when the risk score crosses a threshold." The risk engine computes the score; the policy decides what to do with it.
What the risk engine looks at
Section titled “What the risk engine looks at”Signals in roughly increasing order of weight:
- IP reputation. Is the source IP on a denylist (Tor exit nodes, known-VPN ranges, abuse-source databases)? Configurable threat-intel feeds (FireHOL, AbuseIPDB, custom URL) feed the score.
- Device fingerprint. Is this device recognised (same browser fingerprint that's signed in before from the same user)? New device → bump score.
- Geo distance. How far is this IP geographically from the user's last sign-in? Impossible-travel (London at 9 AM, Singapore at 9:30 AM) is a strong signal.
- Sign-in velocity. Are multiple sign-ins happening rapidly for the same user? Could be password-spraying.
- User-agent oddities. Headless browsers, very old User-Agents, missing headers — all worth noting.
- Time of day. Sign-in at 3 AM local time when the user's pattern is 9-to-5 → suspicious.
The engine combines these into a single score. The exact formula isn't surfaced (it's tuned per-tenant), but the score sits roughly on a 0-100 scale.
What the policy does with the score
Section titled “What the policy does with the score”The tenant admin sets policy thresholds:
- Score < 30 — low risk. Default flow, no extra friction.
- Score 30-60 — medium risk. Require MFA (even if not normally required).
- Score 60-80 — high risk. Require MFA + CAPTCHA.
- Score > 80 — deny outright. Don't even let the user attempt to sign in from this context.
These thresholds are configurable per-tenant; the numbers above are typical defaults.
How your code sees risk
Section titled “How your code sees risk”Three error codes your SDK might return:
risk_challenge— the platform wants you to render a CAPTCHA before continuing. The error includes a challenge payload your code passes to a CAPTCHA component.mfa_required(when normally not) — the risk engine bumped this sign-in to require MFA. Your code triggers the MFA challenge flow; from your code's perspective, it looks identical to "MFA is always required."access_denied— the platform refused the sign-in outright. The risk score crossed the deny threshold. The user sees a "We couldn't sign you in. Try again or contact support." message.
Handle these in your error branch:
const { user, error, loginWithRedirect } = useIntelliAuth()
if (error?.code === 'risk_challenge') { return <CaptchaWidget challenge={error.challenge} onComplete={loginWithRedirect} />}if (error?.code === 'access_denied') { return <Banner>Sign-in temporarily blocked. Please try again later.</Banner>}What the audit log shows
Section titled “What the audit log shows”Every risk decision lands in the tenant's audit log:
risk.score_computed— the risk score for a specific sign-in attempt.risk.threshold_triggered— which threshold tripped (mfa-required, captcha-required, deny).risk.feed_hit— a threat-intel feed matched the IP (if applicable).
Useful for incident response — "did this account get hit by a credential-stuffing attack?" reads cleanly from these events.
What you don't have to write
Section titled “What you don't have to write”You don't write the risk engine. You don't tune the thresholds. You don't maintain the threat-intel feeds. You don't store device fingerprints. The platform does all of that.
What you do write:
- Error handlers for
risk_challenge,mfa_required, andaccess_denied. - A CAPTCHA component (or use one of the SDK's recommended ones).
- A way to surface "try again" to users who hit the deny threshold (so they don't repeat the same path and burn more time).
Adaptive auth is one of the quiet wins of a managed auth platform — the heavy lifting is invisible to your code; you just react to a few new error codes.