Concepts
Geo policy
Block sign-ins from specific countries — or allow only a curated list — and override per-user when teams travel. Lives alongside the risk engine; the geo gate runs first, the risk score runs second.
The geo policy was built for a specific operator pain point: 2FA/authenticator push-spam from countries your users have never visited. The hard truth is that brute-force credential-stuffing traffic is geographically lopsided — and the cheapest mitigation is a per-project “don't even let these countries try” switch. The travel-grant escape hatch covers the legitimate “but my CTO is on a vacation in Tokyo” case without making operators choose between security and availability.
The three modes
project_geo_policies.mode is the single most important field. It accepts three values:
| Mode | Behaviour | When to use |
|---|---|---|
off | Geo checks skipped entirely. The risk engine still uses country as a feeder signal for new_country / impossible_travel. | Default for new projects. |
block | countries[] is a deny-list. Everything else is allowed. | You see attack traffic from a handful of countries you don't do business in. Cap is 50 entries. |
allow_only | countries[] is the allow-list. Everything else is denied, including unknown countries. | Hard-lockdown tenants. Fintech, defense, healthcare with strict residency requirements. |
Alert-only mode
Setting alert_only = true demotes a would-be block to an annotated auth.geo_alert event plus a contribution to the risk score (default +20, named country_in_policy_alert in the signal catalog). The request still completes; nothing visible breaks. Use this to shadow-deploy a new country list and watch the dashboard's “Recent geo-blocks” table for false positives before flipping the kill switch.
Flow scoping
Each row has an applies_to_* flag per auth flow. Defaults are sensible:
applies_to_passkey,applies_to_magic_link,applies_to_oauth,applies_to_step_up→ all true by default.applies_to_session_refresh→ false by default. Users mid-session shouldn't be ejected the moment their plane lands in a new country; this is the single most-asked-for default after the feature shipped.
Travel grants
A user_travel_grant is a per-user, time-bounded override. When the geo gate would block, it queries user_travel_grants for an active row covering the request country (or allow_any_country = true). A match demotes the block to allow — and stamps a geo_grant_used: tgt_… annotation on the risk_decisions row so operators can audit which grant covered which attempt.
Grant constraints:
- Max 365 days. Issuing a longer window returns 400. Renewal is a fresh grant.
- Either a
countries[]list ORallow_any_country = true. Empty countries +allow_any_country = falseis rejected. - Revoke is one-way. A revoked grant cannot be un-revoked — issue a new one if you change your mind.
Order of evaluation
For every primary-authentication request:
- Resolve the country via our MaxMind / DB-IP lookup. If Cloudflare also stamped
CF-IPCountryand they agree, use that; if they disagree, prefer our lookup and loggeoip.cloudflare_disagreement. - Run the geo policy. If
mode = offor the flow is out of scope, skip. Else evaluate block/allow_only + anonymous-proxy / satellite-provider trip-wires. - If the geo policy says block AND there is no active travel grant covering this country → return 403
{ error: "blocked_by_geo_policy", country }immediately. No risk score computed, no step-up issued. - Otherwise, compute the normal risk score with all signals. The geo evaluator's outcome (allow / alert / grant-used) is annotated on
risk_decisions.signalsfor the dashboard. - Decision routes to allow → mint session, step-up → persist challenge, block → 403 (the existing risk-policy path).
Geo-IP data source
auth-core ships with a small embedded country fixture so it boots even when the upstream MaxMind / DB-IP feed is unreachable. Production picks up real data via:
AUTHIO_GEOIP_DB_PATH— operator pre-staged.mmdbfile. Highest priority.MAXMIND_LICENSE_KEY— auth-core downloads GeoLite2-Country from MaxMind weekly and caches it to/tmp/geoip-country.mmdb.- DB-IP free
country-lite— fallback when no MaxMind key. Monthly cadence, no account required.
A loud geoip.fallback_to_fixture warning lands in the auth-core logs when both sources fail. The fixture covers the top ten countries by Authio MAU and exists only to keep the service answering — not as a substitute for real data.
See also
- Risk engine — the per-sign-in scoring layer that the geo policy sits on top of.
- Audit log — every geo event lands as
auth.geo_blocked,auth.geo_alert, orauth.geo_grant_usedfor export.