Aller au contenu principal

Anomaly Detection

Every successful sign-in into a SyAuth account is run through a chain of lightweight detectors. When something looks off — a device we haven't seen, a country the user has never logged in from, or a session that couldn't possibly be the same physical person — SyAuth records a SecurityAnomaly, emails the user, and surfaces a "That was me / That wasn't me" card in their security dashboard.

This is the category Auth0 sells as Adaptive MFA and charges paid tiers for. We ship it on every plan.


Detectors

All detectors run on the server-side session object (ExtsUserSession) immediately after it's created. A failure in one detector never breaks a login and never blocks the others.

new_device

Unseen (browser-family, OS-family, IP) combination for this user. We don't flag on minor Chrome patch changes — UA is normalised to a coarse family (e.g. chrome:mac_os). The user's very first session is never flagged.

Severity: warn.

new_ip

Exact IP address never seen before for this user, but the device family has. Informational — a common false-positive is a coffee shop on a laptop the user already uses.

Severity: info.

new_country

First successful sign-in from a given ISO-3166 country. Requires geo data.

Severity: warn.

impossible_travel

Looks at the most recent prior session (within 24 h) that has geo coordinates. Computes great-circle distance and time delta; if the implied speed exceeds ANOMALY_IMPOSSIBLE_TRAVEL_KMH (default 800 km/h — jet speed), flags.

Severity: critical.


Geolocation

IP → (country, city, lat, lon) is resolved via ipinfo.io. Configure with:

IPINFO_TOKEN=<your token>
IPINFO_CACHE_TTL=86400 # optional, 24h default

Results are cached in Redis (so we don't burn ipinfo quota re-looking up the same IPs) and written back onto the ExtsUserSession row for later audit/display.

Without a token, new_country and impossible_travel are silently disabled. new_device and new_ip still work. This is deliberate — the feature degrades cleanly rather than erroring out if geo isn't configured yet.

Swapping the geo backend is a ~20-line change to geo_service.py — MaxMind GeoIP2 database, ipregistry, etc.


User experience

On the SyAuth dashboard

The Active Sessions page (/security/sessions) shows a Recent security activity section above the device list. Each anomaly shows:

  • Severity badge (info / warning / critical).
  • Event kind (new_device, new_country, …).
  • A plain-English summary: "First sign-in from FR (Paris)", "Impossible travel: 8,200 km in 1.2 h (~6,800 km/h)".
  • When and from which IP / location.
  • A That was me button that acknowledges the anomaly.

If the user reports that wasn't me, they can revoke the matching session one row down in the same page (which pushes both refresh + access jti to the TokenDenylist — see the Sessions doc).

Email alert

When a new_device, new_country, or impossible_travel anomaly fires, SyAuth sends a templated email to the user via the existing EmailTemplate pipeline. Customise the copy by creating a new_device_alert template on the OAuth client:

subject: "New sign-in to your {{ oauth_client.name }} account"
body: |
We noticed a sign-in that looked unusual:

{{ anomaly.message }}

Location: {{ city }}, {{ country }}
IP: {{ ip_address }}
When: {{ when|date:"Y-m-d H:i T" }}

If that was you, you don't have to do anything.

If it wasn't, sign in and revoke the session immediately:
{{ sessions_url }}

Emails can be disabled globally:

ANOMALY_EMAIL_ON_NEW_DEVICE=False

API reference

GET /user/security/anomalies/

Lists the authenticated user's anomalies. Requires a valid access token.

Query params: limit (1–200, default 50).

{
"results": [
{
"id": "6f...",
"kind": "impossible_travel",
"severity": "critical",
"ip_address": "203.0.113.42",
"country": "FR",
"city": "Paris",
"user_agent": "Mozilla/5.0 ...",
"message": "Impossible travel: 8200 km in 1.2 h (~6833 km/h)",
"metadata": {
"from_country": "US",
"to_country": "FR",
"distance_km": 8200.4,
"delta_hours": 1.2,
"speed_kmh": 6833.7,
"prior_session_id": "2a..."
},
"session_id": "3c...",
"notified_at": "2026-04-18T10:15:30Z",
"acknowledged_at": null,
"created_at": "2026-04-18T10:15:28Z"
}
]
}

POST /user/security/anomalies/{id}/ack/

Acknowledge an anomaly ("that was me"). Sets acknowledged_at. Idempotent — a second call is a no-op.


Audit trail

Two workspace-level audit events are emitted. Both live in the Audit Log alongside every other security-sensitive action.

EventSeverityWhen
anomaly_detectedwarn / infoA detector fired during session creation. metadata.kind tells you which detector.
anomaly_notifiedinfoThe user was emailed about one or more anomalies. metadata.kinds is the list.

Because each anomaly also references the triggering ExtsUserSession via target_id, you can pivot from an incident ticket back to the exact session, and from there to every other event (login, subsequent API calls, revocation) tied to that jti.


Operational notes

  • Non-blocking. Detection runs after the session row is safely committed. If geo lookup hangs on a slow ipinfo response (2.5s timeout), the user's login still finishes — we just lose the geo-dependent detectors for that one session.
  • No PII shipped offline by default. ipinfo is only queried when you opt in; there's no "free-tier fallback service" silently sending user IPs elsewhere.
  • Cheap at scale. Every detector is a one-query check against the user's own ExtsUserSession history plus an optional geo lookup. No background worker, no ML inference, no MaxMind dependency.
  • Tunable thresholds. ANOMALY_IMPOSSIBLE_TRAVEL_KMH defaults to 800 (jet cruise). Drop it to 500 for a more aggressive policy; raise it if your user base travels a lot through high-latitude shortcut routes that inflate great-circle speeds.
  • Retention. Rows are kept indefinitely. Add a nightly cleanup job if you want to trim acknowledged rows older than 90 days.

What's not here (yet)

Deliberately deferred — the constants and shape are ready when you need them:

  • Credential-stuffing signal on failed-login bursts. We emit login_failure to the audit log today; a detector that counts per-IP failures over a rolling window and flips the account into MFA-required mode is a short follow-up.
  • Step-up MFA on critical anomalies. impossible_travel could refuse the session and force a fresh MFA verification before issuing tokens.
  • Workspace-level policy. Per-workspace settings (e.g. "always alert on new country") rather than the current global defaults.
  • Machine-learned risk score. A single risk_score 0-100 composed from the individual signals, with a configurable threshold for step-up.