Skip to main content
Account takeover is a known, legitimate account suddenly accessed by someone else: the right password from the wrong place. The shape on the wire is a UserHID you have seen many times before, arriving on a DeviceID you have never seen for it, often from a new country or behind a datacenter, VPN, or Tor session. ShieldLabs gives you the durable DeviceID to recognize the device and the Risk Score to read the session’s anonymity, so your login code can step up to a second factor exactly when the device or location does not fit the account.

Device continuity

The server-derived DeviceID is durable: it survives cleared cookies, incognito, and IP rotation, so a returning customer keeps the same id and an intruder’s machine shows up as a new one.

Session anonymity

The per-request Risk Score folds datacenter, VPN, proxy, Tor, and anti-detect signals into one number, raising the takeover risk when the session is masked.
ShieldLabs scores the session and links the device to the account over time. Your login endpoint owns the verdict: allow, challenge, hold for review, or block. The play here is narrow and high-signal, so reserve the strongest friction for the logins where both the device and the location are new.
This recipe is the device-and-location half of login security. The step-up 2FA recipe owns the threshold ladder that turns a risky login into a second-factor challenge, and the credential stuffing recipe owns throttling the flood of attempts by DeviceID. This page assumes both and only carries the device-comparison logic that is unique to takeover.

The takeover signal: a new device on a known account

A first-time login looks the same to your password check whether it is the real owner on a new laptop or an intruder with a stolen password. The difference is in the history. Before issuing the session for an established account, pull the devices and countries that UserHID has used before and compare them to the one in front of you right now. The cheap, durable comparison key is the DeviceID. It is derived server-side from stable device characteristics, so it stays the same when the visitor clears cookies, opens an incognito window, or rotates their IP, and an intruder on a different machine cannot reproduce it. A UserHID that has only ever appeared on one or two DeviceIDs, now logging in from a third, is the core takeover shape.
Compare the login device against the account's history
// Runs after your password check passes, before you issue the session.
async function takeoverRisk(userHid, shield) {
  const score    = shield?.score ?? 0;       // 0-100, this login's anonymity
  const deviceId = shield?.deviceID;
  const country  = shield?.country;

  // Pull the account's recent sessions. Each returned row bills 1 request,
  // so cap the limit and reserve this read for accounts worth protecting.
  const rows = await shieldHistory('user_hid', userHid, 50);

  const knownDevices   = new Set(rows.map((r) => r.DeviceID).filter(Boolean));
  const knownCountries = new Set(rows.map((r) => r.Country).filter(Boolean));

  // The all-zero DeviceID is "no device", not a new one. Do not treat it as
  // unseen, or every blocked or JS-disabled login looks like a fresh device.
  const NIL = '00000000-0000-0000-0000-000000000000';
  const usableDevice = deviceId && deviceId !== NIL;

  const newDevice  = usableDevice && !knownDevices.has(deviceId);
  const newCountry = country && knownCountries.size > 0 && !knownCountries.has(country);

  return { score, newDevice, newCountry };
}
shieldHistory is the same GET /history/{type}/{value} read the other recipes use, here keyed by user_hid to pull the account’s own recent sessions. The response is an array of snapshots, newest first, each carrying the DeviceID, Country, and Score for that session.
The account's recent devices and countries
curl "https://api.shieldlabs.ai/{domain}:{secret}/history/user_hid/a1b2c3d4hasheduserid?limit=50"
A blocked or JavaScript-disabled browser returns the all-zero DeviceID (00000000-0000-0000-0000-000000000000) and a fixed Risk Score of 90, since the device characteristics needed to derive a stable id were never collected. Route that login to verification on the score alone, and skip the new-device comparison for it. The all-zero id is the absence of a device, so never count it as a new one.

Wire it into your login gate

The verdict combines the device comparison with the session’s anonymity. A new DeviceID on its own can be a real customer’s new phone. A new device and a new country, or a new device and datacenter or Tor signals on the session, is the combination worth a step-up. The step-up 2FA recipe owns the band ladder; here it is the inputs that change the rung.
api/login.js escalate a takeover-shaped login
app.post('/api/login', async (req, res) => {
  const { username, password, shieldRequestId } = req.body;

  // 1. Your normal credential check first.
  const user = await verifyPassword(username, password);
  if (!user) return res.status(401).json({ error: 'invalid_credentials' });

  // 2. Wait briefly for the score; fall back to a History read by request_id.
  //    waitForScore and the webhook handler are the shared Cookbook helper.
  const shield = await waitForScore(shieldRequestId, 2000);
  if (!shield) {
    // Missing data is not "clean". Default an established account to a second factor.
    return res.status(200).json({ status: 'require_2fa', reason: 'verifying' });
  }

  // 3. The device-and-location comparison unique to takeover.
  const { score, newDevice, newCountry } = await takeoverRisk(user.hashedId, shield);

  // 4. Combine. A new device plus a new country, or a new device on an
  //    anonymized session, escalates. Branch on the Score band and the
  //    boolean facts above, never on a Details Description label string.
  if (newDevice && (newCountry || score >= 60)) {
    await alertAccountOwner(user.id, shield);          // notify the real owner
    return res.status(200).json({ status: 'verify', method: 'strong' });
  }
  if (newDevice || score >= 30) {
    return res.status(200).json({ status: 'require_2fa', method: 'otp' });
  }

  // Known device, clean session: issue the session, no extra friction.
  return issueSession(user, res);
});
Step up, do not hard-block. A real customer buys a new laptop, travels, or signs in over a corporate VPN, and every one of those raises the same flags. A second factor or a re-verification keeps the genuine owner in while still stopping an intruder who only has the password. Reserve an outright block for an account already under an active attack, and tune the thresholds against your own login traffic.

Watch the takeover patterns on the dashboard

The History read above reconstructs one account’s device history on demand. For the standing view across all accounts, the dashboard Abuse Patterns compute the same shapes historically and grade each flagged account Suspicious, then Dangerous, as the evidence accumulates. Below the Suspicious threshold an account is the unflagged baseline, which is never recorded or emitted. Patterns are dashboard-only; they do not ride on the webhook.
An account appearing from a (device, country) combination it has never used before. The closest single pattern to a takeover, and the one to watch first: pull its flagged UserHIDs and feed them into a step-up watchlist.
One account reached from many distinct devices over the window. A spread that can mean a shared or resold account, and at the high end an account being worked from a string of new machines.
The same account appearing from several countries in a short span, the impossible-travel shape that VPN hopping and a hijacked session both produce.
An account whose DeviceID or VisitorID keeps changing, a hint of anti-detect tooling cycling its environment between attempts to look like a new visitor each time.
Export the flagged entities from the dashboard Patterns tab as CSV or JSON and use the Dangerous UserHIDs as a step-up watchlist at your login gate: any session for one of those accounts gets a second factor regardless of the per-login score.
Identity continuity rests on the DeviceID, not the VisitorID. The DeviceID is durable across a cookie clear; the VisitorID is recomputed from the device and a browser cookie, so clearing cookies gives the same browser a fresh VisitorID. Compare the device a UserHID arrives on against the DeviceIDs it has used before, and treat a VisitorID change as a weaker hint, not the primary key.
A guide, not a rule. Layer the conditions: a takeover login trips more than one, and friction should rise as they stack.
Condition for an established accountSuggested login action
Known DeviceID, clean session (Score under 30)Allow
New DeviceID, otherwise cleanRequire a second factor
New DeviceID and new CountryRequire strong verification, alert the owner
New DeviceID and High-band anonymity (datacenter, VPN, Tor)Require strong verification, alert the owner
All-zero DeviceID (blocked or JS-disabled, fixed Score 90)Route to verification on the score alone
UserHID on the “New Device and New Country” Dangerous watchlistStep up on every session until reviewed

Next

Step-up 2FA on Risky Logins

The threshold ladder this recipe escalates into: when a risky login becomes a second-factor challenge.

Slow Down Credential Stuffing

The other login defense: throttle the flood of attempts on the durable DeviceID before takeover is even on the table.