Fingerprinting
Every time a sensitive request reaches the IAM service, the getFingerPrint middleware extracts a composite device fingerprint from the incoming request. The fingerprint combines IP-based geolocation data (country, city, coordinates, ISP, proxy/hosting flags) with parsed user-agent fields (browser, OS, device type, vendor, model) and a bot detection signal. This data is attached to req.fingerPrint and flows into three downstream systems: anomaly detection, MFA verification, and visitor trust management.
The fingerprint is not a browser-side fingerprint (no canvas hashing, no WebGL probing). It is a server-side composite built entirely from request headers and IP metadata. The service compares incoming fingerprints against stored visitor records to detect device changes, geographic shifts, and suspicious infrastructure usage.
Data collection
The getFingerPrint middleware runs on every route that involves session verification, token rotation, or MFA. It calls two functions from the Bot Detector package: getGeoData for IP geolocation and parseUA for user-agent parsing.
import { getFingerPrint } from '@riavzon/auth'
// Mount on any route that needs fingerprint data
router.post('/sensitive-action', getFingerPrint, async (req, res) => {
const { ipAddress, country, city, browser, os, device } = req.fingerPrint
// ...
})
The middleware populates req.fingerPrint with the full FingerPrint interface:
Geolocation fields
req.ip. Used as the primary key for geo lookups and anomaly comparisons.'Germany').'DE').'BE' for Berlin).'Berlin').'Berlin').'Europe/Berlin').'EUR').true and the user has not been verified via MFA.proxy.User-agent fields
User-Agent header string, stored as-is for logging and comparison.'desktop', 'mobile', 'tablet').'Apple', 'Samsung').'iPhone', 'Galaxy S24').'Chrome', 'Safari', 'Firefox').'125.0.0.0').'macOS', 'Windows 11', 'Android 14').city, lat, and lon depends on the database edition and the client's ISP.Where fingerprints are collected
The getFingerPrint middleware is mounted on routes where the service needs to verify or update the client's identity:
| Route | Purpose |
|---|---|
POST /auth/user/refresh-session | Token rotation. Fingerprint feeds into strangeThings() anomaly checks. |
POST /auth/verify-mfa | Adaptive MFA verification. Fingerprint is persisted via updateVisitors on success. |
POST /custom/mfa/:reason | Custom MFA initiation. Fingerprint metadata is included in the OTP email. |
GET /auth/verify-custom-mfa | Custom MFA link validation. |
POST /auth/verify-custom-mfa | Custom MFA code submission. |
POST /update/email | Email update. Fingerprint is persisted after successful verification. |
GET /auth/reset-password | Password reset link validation. |
POST /auth/reset-password | Password reset submission. |
GET /secret/data | BFF access route. |
GET /secret/accesstoken/metadata | BFF metadata route. |
getFingerPrint middleware directly. They extract device metadata through parseUA and getGeoData inline and pass it to the visitor creation flow.Visitor persistence
When a user completes MFA verification or logs in from a new device, the service calls updateVisitors from the Bot Detector package to persist the fingerprint to the visitors table. This stored record becomes the baseline for future anomaly comparisons.
The visitors table stores one record per visitor_id (linked to the canary_id cookie). Each record contains the full set of geolocation and user-agent fields from the FingerPrint interface. When updateVisitors is called, it overwrites the stored record with the current request's fingerprint data.
When fingerprints are persisted
| Event | Trigger | Function |
|---|---|---|
| Login | Successful credential verification | updateVisitors called with parsed request data |
| MFA verification | OTP code validated | verifyMfaCode calls updateVisitors after token rotation |
| Visitor trust | New device verified | trustVisitor calls updateVisitors inside a transaction |
trustVisitor
trustVisitor is called when a user verifies a new device through MFA. It performs two operations inside a single database transaction:
- Updates the user's
visitor_idto point to the newly verified visitor record - Calls
updateVisitorsto persist the current fingerprint to that visitor record
If updateVisitors fails, the transaction rolls back and the user's visitor_id is not updated. This ensures the user is never associated with a visitor record that has incomplete fingerprint data.
import { trustVisitor } from '@riavzon/auth'
const result = await trustVisitor(
userId,
visitorIdToTrust,
canaryId,
req.fingerPrint,
log
)
if (!result.ok) {
// Transaction failed — visitor not trusted
log.error(result.data)
}
Anomaly detection integration
The stored fingerprint is the reference point for anomaly detection. When strangeThings() runs on a refresh-token use, it joins the refresh_tokens, users, and visitors tables to compare the incoming request against the stored visitor record.
The following fingerprint fields are compared:
| Field | Anomaly type | Result |
|---|---|---|
canary_id cookie vs stored canary_id | Device change | reqMFA: true (new device) |
ipAddress vs stored IP range | Network change | reqMFA: true (different network) |
proxy flag | Proxy detected | reqMFA: true if proxy_allowed = 0 |
hosting flag | Hosting/datacenter detected | reqMFA: true if hosting_allowed = 0 |
device type | Device type change | reqMFA: true |
browser name | Browser change | reqMFA: true |
os name | OS change | reqMFA: true |
lat / lon coordinates | Geographic shift | reqMFA: true (significant distance) |
verifyMfaCode sets proxy_allowed = 1 and hosting_allowed = 1 on the visitor record. This grants trusted status to the verified device, suppressing proxy and hosting anomalies until the next suspicious event resets them.The canary_id lifecycle
The canary_id is a long-lived browser cookie that serves as the persistent device identifier. It ties a specific browser instance to a visitor_id in the database.
Cookie creation
A canary_id cookie is set when a user first interacts with the authentication system (signup or login). The value is a randomly generated identifier.
Storage linkage
The canary_id is stored alongside the user's visitor_id in the visitors table. The users table references the visitor_id, creating a chain: user → visitor_id → visitors table → canary_id.
Anomaly comparison
On every authenticated request, strangeThings() compares the canary_id from the incoming cookie against the value stored in the database. A mismatch indicates a new or different device, triggering an MFA challenge.
XSS integration
When an XSS attempt is detected, handleXSS uses the canary_id to permanently mark the corresponding visitor as a bot and update the banned IP record. This means the ban follows the device, not just the IP.
Accessing fingerprints in custom handlers
After the getFingerPrint middleware runs, the full fingerprint is available on req.fingerPrint. Use it for logging, analytics, or custom security logic.
import { getFingerPrint } from '@riavzon/auth'
router.post('/transfer',
getFingerPrint,
async (req, res) => {
const fp = req.fingerPrint
// Log the request context
log.info({
ip: fp.ipAddress,
country: fp.country,
browser: fp.browser,
device: fp.device,
proxy: fp.proxy,
}, 'Transfer request')
// Block requests from hosting providers
if (fp.hosting) {
return res.status(403).json({ error: 'Requests from hosting providers are not allowed.' })
}
// Include location in the confirmation email
const location = [fp.city, fp.regionName, fp.country].filter(Boolean).join(', ')
await sendConfirmationEmail(user.email, {
amount: req.body.amount,
location,
device: `${fp.browser} on ${fp.os}`,
})
}
)
getFingerPrint middleware fails gracefully. If getGeoData or parseUA throws (,the IP geolocation database is unavailable), the middleware logs the error and calls next() without populating req.fingerPrint. Always check for undefined fields when accessing geolocation data in production.