Anomaly detection
WIPHijack / device-fingerprint / IP-shift detectors that gate session use; aggregated into a single allow / step-up / deny decision.
What this is
AuthEngine.anomaly is a pluggable detector chain. Every call to
resolveSession(req, { requestSnapshot }) runs the registered detectors
and produces a single decision: allow, step-up, or deny.
Detectors are independent functions; the aggregator combines their outputs. Operators branch on the final field rather than re-running every detector at every route.
Built-in detectors
| Detector | What it looks at | When it fires |
|---|---|---|
ipShift | Session ip vs request snapshot IP | Sudden country / ASN change |
userAgentShift | Session userAgent vs request UA | UA family changed mid-session |
fingerprint | Session fingerprint vs request | Canvas / WebGL / font hash mismatch |
replay | Cookie reuse from impossible-distance second request | Stolen-cookie indicator |
Each detector returns { score: 0..1, signals: [...] }. The aggregator
sums and maps:
score < 0.3->allow0.3 <= score < 0.7->step-up(force re-auth via MFA)score >= 0.7->deny(revoke session)
Registration
auth.anomaly.register(ipShift({ allowedAsnDelta: 1 }))
auth.anomaly.register(userAgentShift())
auth.anomaly.register(fingerprint({ hashAlg: 'sha256' }))
Snapshots are captured per-request and passed in:
const resolved = await auth.resolveSession(req, {
requestSnapshot: {
ip: req.ip,
userAgent: req.get('user-agent') ?? '',
fingerprint: req.cookies['fp'] ?? '',
},
})
if (resolved?.anomaly?.decision === 'deny') {
await auth.sessions.revoke(resolved.session.id)
return res.status(401).end()
}
if (resolved?.anomaly?.decision === 'step-up') {
return res.redirect('/auth/mfa?reason=anomaly')
}
Custom detectors
auth.anomaly.register({
id: 'tor-exit',
evaluate: async ({ session, req }) => {
const onTorList = await checkTorBlocklist(req.ip)
return {
score: onTorList ? 0.9 : 0,
signals: onTorList ? ['tor-exit'] : [],
}
},
})
Detector throws are caught by the aggregator; a misbehaving detector cannot wedge the session.