Olympus Docs
CookbookDefensive security

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).

On this page