Obexal Docs

Docs/Access control/Risk-based access

Risk-based access

A deterministic, explainable risk score computed at sign-in from six signals, with per-tenant thresholds that trigger an MFA step-up or a denial.

At every sign-in, Obexal can compute a risk score for the connection from the user's own history (the last 60 days, up to 500 events). There is no machine-learning black box: the signals are fixed, the result is reproducible, and every elevation is audited with the exact signals that fired.

A deterministic and explainable score

Each signal that fires adds a fixed amount to the score, so the same history always yields the same result. Because the computation is deterministic, you can always answer the question "why was this user challenged": the answer is in the audit log, signal by signal.

The six signals

SignalFires when
new_ipThe source IP has never been seen in a successful sign-in of this user
rapid_ip_changeA successful sign-in from a different IP happened a very short time earlier
failed_burstSeveral recent failed attempts suggest a credential stuffing pattern
new_deviceThe User-Agent has never been seen in a successful sign-in
off_hoursThe hour of day (UTC) has never been seen, evaluated only once enough activity history exists
impossible_travelThe move since the last successful sign-in is physically impossible

new_ip and new_device require at least one prior successful sign-in, so a brand-new user is not penalized on the first connection. off_hours waits until enough successful sign-ins of history exist before judging an hour unusual.

Impossible travel

The countries of the current IP and of the last successful sign-in are resolved with the local GeoIP database (no external API). From the distance between the two countries and the time elapsed, the signal fires when the implied travel speed between the two sign-ins would be physically impossible.

The detection is deliberately conservative: it only evaluates sufficiently distant jumps, so a short commute across a nearby border (France to Belgium, for instance) can never look "impossible". No GeoIP database, an unresolvable country or the same country on both sides produces no signal.

Thresholds and actions

The score is compared to two thresholds you set for your organization: at or above mfaThreshold an MFA step-up is required, and at or above denyThreshold the sign-in is refused. You choose how much accumulated risk elevates a sign-in to a step-up and then to a refusal; denyThreshold is kept greater than or equal to mfaThreshold, so a denial is always the stricter of the two.

Both endpoints require the tenant:manage permission with an Admin API token. A custom domain replaces accounts.obexal.com.

curl -sS https://accounts.obexal.com/v1/admin/risk-policy \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN"
# {"policy": {"enabled": false, "mfaThreshold": 3, "denyThreshold": 6}}

curl -sS -X PUT https://accounts.obexal.com/v1/admin/risk-policy \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "mfaThreshold": 3, "denyThreshold": 6}'
# 204 No Content

Combined with conditional access

The risk action is merged with the decision of the static conditional access rules: the strictest wins (allow < require_mfa < deny). Risk can only tighten a decision, never relax a static deny. As with conditional access, a require_mfa hitting a user with no enrolled factor refuses the sign-in.

Opt-in and availability first

Risk-based access is disabled by default (enabled: false): you turn it on per tenant, ideally after watching the audit log for a while to calibrate thresholds.

The risk layer is designed to never block a legitimate sign-in when a signal is unavailable: if the policy is disabled or a signal cannot be evaluated, the sign-in proceeds normally and no elevation is applied. This is a deliberate trade-off in favor of availability over an unverifiable elevation. If you need a hard guarantee, express it as a static conditional access rule, which is what the risk layer builds upon.

Every elevation is audited

Any risk action other than allow writes an auth.login.risk_elevated event to the audit log, carrying the action taken, the score and the list of signals, for example ["new_ip", "failed_burst"]. This makes every challenge and every refusal explainable after the fact.