Orange textured background

Anti-fraud use case

Prevent account sharing

How to detect password sharing end to end: store a distinct hardware fingerprint for each device that accesses an account, count them, and flag when the count crosses your policy threshold.

Password sharing lets one paid subscriber's credentials serve many users across many devices. This is the end-to-end architecture for a device-count gate: what you store per account, how the distinct-device count runs on each login, and how the threshold flag works.

The job

A subscriber account is sold to one person or one household. When credentials are shared beyond that boundary, your per-seat revenue is diluted: a single paid subscription serves two, five, or twenty active users across as many devices. IP-based detection fails because a household can span several IPs, and a credential-sharing ring may operate across separate households. Concurrent-session limits are easy to defeat by staggering login times.

The durable signal is the count of distinct physical devices that have accessed the account. Each device produces a unique `hardwareFingerprint`, and that fingerprint is stable across browser switches, cookie clears, and incognito mode. When a single account accumulates more distinct device fingerprints than your policy allows, sharing is in progress. This page covers the full architecture for that check: what you store per account, how the count runs on each login, and what the threshold decision looks like.

What makes the accountId key invert the pattern

The other use-case gates (free-trial abuse, multi-accounting, referral fraud) key their store on `hardwareFingerprint` and count accounts per device. Account sharing inverts this: the store is keyed on `accountId` and the count is of distinct `hardwareFingerprint` values per account. One account row holds a set of device hashes; when that set grows beyond your threshold N, the account is flagged.

`getFingerprint` still returns `hardwareFingerprint` from the client on each login, and that hash is what you add to the account's device set. The `hardwareFingerprint` is stable across browsers on one physical device, so a user switching from Chrome to Safari on the same laptop does not add a new entry to the set. A user on a genuinely new device does.

The end-to-end flow

StepWhat happens
1. CaptureAt each login, call getFingerprint() client-side. You get fingerprint, hardwareFingerprint, and consistency. No server involved yet.
2. SendPOST the two hashes plus the consistency result to your backend over your existing API along with the account credentials. Decide server-side: never trust a verdict computed in the browser.
3. StorePersist a record keyed by accountId: { accountId, devices: [{ hardwareFingerprint, fingerprint, firstSeen, lastSeen }] }. One account row holds the set of distinct devices that have accessed it. You own this store; the library is client-only.
4. Look upOn each login, load the account's device set and check whether the incoming hardwareFingerprint is already present. For hardware drift tolerance, run compareFingerprints in cross-browser mode against stored device records for a matchScore.
5. DecideIf the incoming device is new (not in the set and no high matchScore) and the set is already at your threshold N, the login is flagged. Layer in consistency.spoofLikelihood to catch anti-detect browsers generating fresh hardware identities.
6. Act and logBlock new device logins over the limit, require a plan upgrade, or trigger a re-authentication flow. Log which device was flagged and update lastSeen on the matched device record.

The decision rule

The account-sharing rule is a count of distinct devices per account. You set the threshold N based on your product policy: one device for a strictly personal plan, or a small household number (for example, up to four devices) for a family tier. When a login from a new device would push the set count above N, block or challenge the new device login.

Account-sharing gate by device count

ConditionOutcome
Device already in the account's setKnown device: allow login, update lastSeen.
New device, account device count below threshold NUnder limit: add device to the set and allow login.
New device, account device count at or above threshold N, low spoofLikelihoodOver limit: block new device login. Prompt to remove an old device or upgrade the plan.
High spoofLikelihood on the incoming deviceEvasion signal: challenge before allowing login regardless of device count.
No exact match, but high matchScore in cross-browser mode against an existing deviceProbable returning device: allow login and update the matched record rather than adding a new entry.
npm install doorman-benny
import { getFingerprint } from 'doorman-benny';

// STEP 1-2: capture in the browser on every login, send hashes to your backend.
async function submitLogin(credentials: { email: string; password: string }): Promise<void> {
  const result = await getFingerprint();

  await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...credentials,
      hardwareFingerprint: result.hardwareFingerprint,
      fingerprint: result.fingerprint,
      spoofLikelihood: result.consistency.spoofLikelihood,
    }),
  });
}

Client side. Capture on every login. The device count decision runs on the server against the account's stored device set.

import { compareFingerprints } from 'doorman-benny/server';

// STEP 3-6: store, count distinct devices, and decide on the server.
// `db` is your own datastore, indexed by accountId.
// MAX_DEVICES_PER_ACCOUNT is your own policy threshold (e.g. 4 for a family plan).
const MAX_DEVICES_PER_ACCOUNT = 4;

async function evaluateLogin(payload: {
  accountId: string;
  hardwareFingerprint: string;
  fingerprint: string;
  spoofLikelihood: 'low' | 'medium' | 'high';
}): Promise<{ allowed: boolean; reason: string }> {
  if (payload.spoofLikelihood === 'high') {
    return { allowed: false, reason: 'challenge-required' };
  }

  const account = await db.accounts.findById(payload.accountId);
  const devices = account?.devices ?? [];

  // Check for an exact match first (returning device, possibly same browser).
  const exactMatch = devices.find(
    (device) => device.hardwareFingerprint === payload.hardwareFingerprint
  );
  if (exactMatch) {
    await db.accounts.updateDeviceLastSeen(payload.accountId, payload.hardwareFingerprint);
    return { allowed: true, reason: 'known-device' };
  }

  // Cross-browser comparison to catch a returning device that switched browsers.
  for (const stored of devices) {
    const comparison = compareFingerprints(
      payload.fingerprint,
      stored.fingerprint,
      { mode: 'cross-browser' }
    );
    if (comparison.hardwareMatch) {
      await db.accounts.updateDeviceLastSeen(payload.accountId, stored.hardwareFingerprint);
      return { allowed: true, reason: 'cross-browser-match' };
    }
  }

  // Genuinely new device: check the count before adding.
  if (devices.length >= MAX_DEVICES_PER_ACCOUNT) {
    return { allowed: false, reason: 'device-limit-reached' };
  }

  await db.accounts.addDevice(payload.accountId, {
    hardwareFingerprint: payload.hardwareFingerprint,
    fingerprint: payload.fingerprint,
    firstSeen: new Date(),
    lastSeen: new Date(),
  });
  return { allowed: true, reason: 'new-device-within-limit' };
}

Server side. The store is keyed by accountId and holds the set of distinct device records. Cross-browser comparison catches a returning device that switched browsers before adding a new entry to the set.

Which signals and which mode

The account-sharing gate counts distinct hardware-bound device identities per account. The `hardwareFingerprint` is the right key because it is stable across browsers on one physical device: a user who switches from Chrome to Firefox is still the same device and should not count twice.

Use exact `hardwareFingerprint` equality for the fast path. Fall back to `compareFingerprints` in `cross-browser` mode when you need to tolerate minor hardware drift: a returned device that has had an OS update, added an external monitor, or changed a USB device may produce a slightly different raw hash but will still show `hardwareMatch: true` in cross-browser mode.

Handling false positives

Legitimate multi-device use within a household is the expected case for many subscription products. Set your threshold N to reflect your product's intended scope: a personal plan at one or two devices, a family plan at four to six. Enforce the threshold by prompting users to remove stale devices rather than hard-blocking the login.

When a known device's hardware hash drifts slightly after an OS update, the cross-browser mode comparison prevents it from counting as a new device. The `hardwareMatch` field gives a binary answer that is more reliable than raw string equality for this case.

Devices that have not been seen in a long time (using `lastSeen` as an age gate) are candidates for automatic expiry from the device set, which frees up slots for the genuine user's current devices.

Frequently asked questions

Why key the store on accountId instead of hardwareFingerprint for account sharing?

Because the fraud pattern is the inverse: instead of one device spawning many accounts, one account is accessed by many devices. Keying on accountId lets you hold the complete set of devices per account and count them directly. The hardwareFingerprint is still the value you store in that set.

Does switching browsers count as a new device?

No. The hardwareFingerprint is derived from hardware-bound signals that produce the same value across Chrome, Firefox, and Safari on one physical device. A user switching browsers on the same machine adds no new entry to the device set; only compareFingerprints in cross-browser mode is needed to confirm the match.

What should I do when a user hits the device limit legitimately?

Show a device management screen where they can review active devices and remove old ones (a phone they no longer use, a laptop they replaced). This is both better UX than a hard block and a natural conversion path for users who genuinely need more devices to upgrade their plan.

How do I handle a device that has shifted after an OS update?

Use compareFingerprints in cross-browser mode rather than raw equality. The hardwareMatch field returns true when the hardware-bound signals align between two records, even if minor values like reported platform version have changed. A match updates lastSeen on the existing device record rather than adding a new entry.

Is this compliant with GDPR or ePrivacy?

Fingerprinting for fraud prevention generally qualifies for a security and fraud-prevention exemption under GDPR and most ePrivacy frameworks, provided the processing is proportionate and disclosed in a privacy notice. The laws/fingerprinting-anti-fraud-exemption page covers the landscape. This is engineering documentation, not legal advice.

Get started

Build the device-count sharing gate

npm install doorman-benny. Hardware-bound device identity, cross-browser matching, and free anti-spoof scoring in one package. No cookies, no server round-trip for the fingerprint.

Get startedRead the how-to

Related use cases

Last reviewed June 20, 2026