Device fingerprinting on login; new device triggers step-up
Why it matters
Password and OTP credentials are portable — they can be phished, replayed from a different device, or used by a credential-sharing household member. Device fingerprinting adds a possession factor derived from the actual browser and hardware environment, making a stolen credential set insufficient on an unrecognized device. CWE-287 (Improper Authentication) and OWASP A07 identify the gap between credential verification and device trust. NIST 800-63B IA-3 addresses device identification requirements. In financial fraud patterns, credential stuffing attacks typically originate from devices that have never logged into the account — fingerprinting flags this at first login from a new device and triggers step-up before any transaction is possible.
Severity rationale
Medium because stolen credentials alone are sufficient to authenticate from an unrecognized device without triggering any additional verification, converting a phishing success directly into financial account access.
Remediation
Integrate FingerprintJS or equivalent in src/lib/deviceFingerprint.ts and wire a step-up trigger for unrecognized devices in the login handler:
import FingerprintJS from '@fingerprintjs/fingerprintjs';
export async function getDeviceFingerprint(): Promise<string> {
const fp = await FingerprintJS.load();
const result = await fp.get();
return result.visitorId;
}
// In login handler
const fingerprint = await getDeviceFingerprint();
const knownDevices = await db.userDevices.findMany({ where: { userId } });
const isKnown = knownDevices.some(d => d.fingerprint === fingerprint);
if (!isKnown) {
// New device — block and require step-up before completing login
return requireStepUp(session, fingerprint);
}
Store enrolled device records in a user_devices table with fingerprint hash, enrollment timestamp, and a user-readable label (e.g., "MacBook Chrome"). Provide a UI for users to view and revoke trusted devices.
Detection
- ID:
device-fingerprinting - Severity:
medium - What to look for: Count all device fingerprinting implementations (FingerprintJS, custom fingerprinting, browser signals). Quote the actual library or fingerprinting method found. Enumerate the signals used (user agent, screen, timezone, hardware). Count all code paths where a new device triggers step-up authentication.
- Pass criteria: At least 1 device fingerprinting mechanism exists that uses at least 3 browser/hardware signals. New device logins trigger at least 1 step-up challenge. Report the count even on pass (e.g., "FingerprintJS with 5 signals, 1 step-up trigger for unrecognized devices, 12 trusted devices per user max").
- Fail criteria: No device fingerprinting (0 implementations), or new devices do not trigger step-up (0 new-device challenges).
- Skip (N/A) when: The application is not web-based or does not have device-specific security requirements — cite the actual platform found.
- Detail on fail:
"0 device fingerprinting implementations — new logins from unknown devices pass without additional verification" - Remediation: Implement device fingerprinting and trust (in
src/lib/deviceFingerprint.ts):// lib/deviceFingerprint.ts import FingerprintJS from '@fingerprintjs/fingerprintjs'; export async function getDeviceFingerprint() { const fp = await FingerprintJS.load(); const result = await fp.get(); return result.visitorId; // Unique device identifier } // In login flow const fingerprint = await getDeviceFingerprint(); const knownDevices = await db.userDevices.findMany({ where: { userId } }); const isKnownDevice = knownDevices.some(d => d.fingerprint === fingerprint); if (!isKnownDevice) { // New device — require step-up auth return requireStepUp(session, fingerprint); }
External references
- cwe · CWE-287 — Improper Authentication
- owasp:2021 · A07
- nist:rev5 · IA-3 — Device Identification and Authentication
Taxons
History
- 2026-04-18·v1.0.0·Initial import from finserv-session-security·automated