Detect impossible travel
Flag logins from physically impossible locations
"Impossible travel" = a user logs in from London, then 10 minutes later from Tokyo. Almost certainly: stolen credentials being used from a botnet, or VPN-hopping. Flag it.
Approach
Olympus's SDK records every session's geo-location in the locations table. Compare each new login to the most recent one:
- If physical distance > N km
- And time delta < M minutes
- Then flag (and/or block, alert, require step-up).
Implementation
Add a check on login completion
In the SDK or your custom Kratos hook:
async function checkImpossibleTravel(identityId: string, newIp: string) {
const latest = await db`
SELECT ip, country, city, created_at
FROM locations
WHERE identity_id = ${identityId}
ORDER BY created_at DESC
LIMIT 1
`.first();
if (!latest) return; // First login, no comparison.
const newGeo = await geoLookup(newIp); // MaxMind, ipdata.co, etc.
const oldGeo = await geoLookup(latest.ip);
const distanceKm = haversine(
newGeo.latitude, newGeo.longitude,
oldGeo.latitude, oldGeo.longitude
);
const minutesSince = (Date.now() - new Date(latest.created_at).getTime()) / 60_000;
// Threshold: 600 km in 30 minutes
const maxKmPerMinute = 600 / 30; // ~20 km/min (faster than commercial flight)
if (distanceKm > maxKmPerMinute * minutesSince) {
return { suspicious: true, oldGeo, newGeo, distanceKm, minutesSince };
}
return { suspicious: false };
}Take action
const check = await checkImpossibleTravel(identity.id, sourceIp);
if (check?.suspicious) {
// Option 1: Force step-up MFA
return { redirect: "/self-service/login/browser?aal=aal2&refresh=true&reason=impossible_travel" };
// Option 2: Email the user
await sendSecurityAlert(identity.email, {
type: "impossible_travel",
oldLocation: `${check.oldGeo.city}, ${check.oldGeo.country}`,
newLocation: `${check.newGeo.city}, ${check.newGeo.country}`,
});
// Option 3: Block outright (aggressive)
return { error: "Login from unusual location blocked" };
}Edge cases
- VPN users: legitimately hop locations. Don't be too aggressive.
- Mobile users on roaming: similar, IP geo flips frequently.
- Power users with global presence: traveling executives, devops, etc.
Tune the thresholds (km, minutes) based on your user base. Start with high thresholds (1000km in 15min) and tighten as you collect data.
Combine with other signals
Impossible travel alone is a weak signal. Combine with:
- Device fingerprint: new browser fingerprint + new location is more suspicious than known browser + new location.
- User agent change: different OS in the same session window.
- Failed-login rate: recent failed logins on the account add suspicion.
Risk-based step-up is the right pattern: any combination of signals can trigger AAL2 without fully blocking.
Privacy
Geo-IP lookups happen server-side. Don't expose user IPs to your frontend. The location stored in the locations table is PII, handle per your DPA.