Device fingerprinting for risk signals
Recognize returning devices for adaptive authentication
A device fingerprint identifies a browser + device combo without explicit identifiers. Useful for:
- "New device" notifications.
- Risk-based step-up.
- Brute-force detection.
But: it's a tracking technology. Use responsibly, document in privacy policy, comply with GDPR/CCPA.
What to fingerprint
Stable signals (combine for better fingerprint):
- User-Agent (browser, OS, version).
- Accept-Language.
- Accept-Encoding.
- IP (variable but useful).
- Geolocation derived from IP.
- Screen resolution.
- Timezone.
- Available fonts (in browser).
- Canvas/WebGL fingerprint (paint a hidden canvas, hash the bytes).
- Audio fingerprint (Web Audio API).
Note: many of these are now restricted in privacy-focused browsers (Brave, Tor Browser, Safari). Don't rely on canvas/WebGL alone.
Simple server-side fingerprint
For a low-effort signal:
function fingerprint(req: Request): string {
const ua = req.headers["user-agent"] ?? "";
const lang = req.headers["accept-language"] ?? "";
const enc = req.headers["accept-encoding"] ?? "";
// Don't include IP, too unstable
return sha256(`${ua}|${lang}|${enc}`);
}Returns a stable hash per browser version. Won't change between sessions.
Limitations: very low entropy. Two users on same browser+OS look identical.
FingerprintJS
Commercial library, much higher accuracy:
<script src="https://openfpcdn.io/fingerprintjs/v4"></script>
<script>
const fpPromise = FingerprintJS.load();
fpPromise.then(fp => fp.get()).then(result => {
document.cookie = `fp=${result.visitorId}; path=/; max-age=31536000`;
});
</script>visitorId is a stable identifier across sessions, even after cookie clearing.
Cost: free tier ~$0 / 100k visitors / mo. Paid for high volume.
Storing fingerprints
For each identity, track seen fingerprints:
CREATE TABLE identity_devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identity_id UUID NOT NULL,
fingerprint TEXT NOT NULL,
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ip_history JSONB,
user_agent TEXT,
trusted BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX ix_devices_identity ON identity_devices(identity_id);On login:
const fp = fingerprint(req);
const known = await db`SELECT * FROM identity_devices WHERE identity_id=${identityId} AND fingerprint=${fp}`.first();
if (known) {
await db`UPDATE identity_devices SET last_seen=NOW() WHERE id=${known.id}`;
// Familiar device
} else {
await db`INSERT INTO identity_devices (identity_id, fingerprint, user_agent) VALUES (${identityId}, ${fp}, ${req.headers["user-agent"]})`;
// NEW device, alert
await sendNewDeviceEmail(identityId, fp, req.headers["user-agent"]);
}"Trust this device"
Let users mark a device as trusted, future logins from it skip MFA:
<Checkbox label="Don't ask for code from this device for 30 days" />On enrollment:
await db`UPDATE identity_devices SET trusted=true, trust_expires=NOW() + INTERVAL '30 days' WHERE id=${deviceId}`;Pre-login check:
const device = await db`SELECT trusted, trust_expires FROM identity_devices WHERE fingerprint=${fp}`.first();
const skipMfa = device?.trusted && device.trust_expires > NOW();Trade-off: convenience vs security. For highest-risk apps, never skip MFA.
Notification
When a new device logs in:
Subject: New sign-in to your account
We noticed a new sign-in:
Date: 2026-05-15 14:32 UTC
Device: Chrome on Windows 11
Approximate location: Seattle, WA (USA)
If this was you, you can ignore this email.
If not, change your password and contact support.Don't include the fingerprint hash. Describe the device (browser, OS).
False positives
A user upgrades Chrome → user agent changes → looks like new device.
Mitigations:
- Don't include patch version in fingerprint.
- Allow user to mark non-suspicious manually.
- Use additional signals (cookie persistence, IP geolocation similarity).
Privacy
Fingerprinting can identify users across services if you sell/share data. Don't.
For users in EU/CA: declare in privacy policy:
- What you fingerprint.
- Why.
- Retention.
Give users a way to opt out (with reduced security as the trade-off).
Defeating fingerprinting
Some users actively defeat:
- Tor Browser standardizes most signals.
- uBlock + Privacy Badger remove some.
- Mobile apps in incognito.
Their fingerprint changes every session. Treat them as "always new device." Step up auth every time, with explanation.
Combine with behavioral
Beyond fingerprint:
- Login time-of-day pattern.
- Locations the user has previously logged in from.
- Mouse movement / typing rhythm (in a session).
A fingerprint match + suspicious behavior = still flag.
A fingerprint mismatch + everything-else-normal = mild flag (might be a legitimate device upgrade).