Server Side Fingerprinting: 4 Privacy Modes
A login from a new device is either a customer on a train or a stolen password, and the log line that could tell the difference is the same one both leave. Server side fingerprinting builds an identifier from what the request already carries, and the question is how much of it you are willing to keep.
Code: Fingerprint-PHP, MIT licensed.
composer require globus-studio/fingerprint, PHP 8.3 and newer, four privacy modes, and trusted proxy handling that refuses to guess.
What it is for
Three places earn the cost. A login from an unrecognised device can ask for a second factor instead of accepting a password. A checkout from an address that has never been seen can go to manual review instead of being declined. And an existing session that suddenly changes device gets logged, which is how a compromised account is found before a refund.
None of those require certainty. Each requires a signal strong enough to raise friction, and that is the part a fingerprint can actually provide.
The four modes
strict collects minimal signals and keeps no full IP and no raw values. balanced is the recommended default for security. maximum raises the signal count and expects a stronger legal basis. custom lets the application name its own policy.
Starting in balanced and moving to maximum only with a documented reason is the sequence a privacy review accepts. Starting at maximum and promising to reduce later is the sequence nobody finishes.
Proxies are where it usually breaks
Behind a CDN or a load balancer every request appears to come from the same address. The fix is a trusted proxy list, not blind header reading: withTrustedProxies(['10.0.0.0/8']) and withTrustedHeaders(['x-forwarded-for']), with forwarded headers ignored unless REMOTE_ADDR is trusted.
That rule blocks the trivial attack where a client sets its own X-Forwarded-For and chooses a fingerprint. It is also the first thing to verify after a migration, because a wrong proxy list produces one identifier for every visitor and an alert that looks like a DDoS.
Matching is a level, not a boolean
compare() returns a match level, and level()->isSuspicious() is the question a login form asks. Two fingerprints never compare equal, because browser versions and screen sizes move.
A boolean “same device” forces a threshold nobody can defend in review. A level lets the application say what it does with a medium match, which is usually: ask for the second factor.
What it cannot do
Fingerprinting is probabilistic. NAT, VPN, corporate proxies, mobile networks, browser privacy settings and updates all change signals, and a user who switches between WiFi and mobile data looks like two devices.
The readme says plainly that a fingerprint must not be the only authentication or blocking factor. A team that reads that sentence designs a second factor; a team that skips it locks out customers on holiday.
What to check on day one
Log the identifier for a week before enforcing anything. The first finding is usually a proxy misconfiguration, and the second is that mobile users produce two or three identifiers a day.
Neither is a reason to abandon the approach; both are reasons to run it in report mode first.
The privacy bill
A fingerprint is personal or pseudonymous data. It needs a legal basis, a retention policy and a privacy review, and the mode is part of that decision rather than a setting for the developer.
Where the decision is written down, the library is a tool. Where it is not, the same library is a liability with a changelog.
Related: PHP development for how an integration like this is scoped, and wordpress plugins development for the plugin context where server-side checks usually run.
