> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Account takeover

> Run the standalone account-takeover app and compare its starter and final versions.

## What you will build

A new device needs another factor. This is an illustrative application policy, not a default rule that ShieldLabs applies to every customer.

## Run the starting application

You need Node.js 22 or later. The starter runs without ShieldLabs keys; the final version needs a registered HTTPS hostname and matching keys.

Clone the public [tutorial repository](https://github.com/ShieldLabs-ai/use-case-tutorials), then start this standalone application:

```sh theme={null}
git clone https://github.com/ShieldLabs-ai/use-case-tutorials.git
cd use-case-tutorials
git switch starter
cd account-takeover
npm ci --omit=dev
cp .env.example .env
npm run dev
```

Open [http://127.0.0.1:3000](http://127.0.0.1:3000). The starter has no ShieldLabs dependency or identification check. Its own SQLite state is separate from every other tutorial.

## Add the integration

Stop the starter server. Follow the changes below. The `final` branch contains the complete runnable implementation.

<Steps>
  <Step title="1. Prepare the keys and HTTPS hostname">
    The `.env` copied from `starter` contains no key entries yet. Add the two lines below with this domain's real values:

    ```dotenv theme={null}
    SHIELDLABS_PUBLIC_KEY=your-public-key
    SHIELDLABS_API_KEY=sec_your_private_api_key
    ```

    Register the hostname for this app in **Integration > Domains**. Put its matching Public Key and Private API Key from **Integration > API keys** in your private `account-takeover/.env` as `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY`. The Private API Key belongs on the server. Serve the Node process through HTTPS on the registered hostname; a customer key does not automatically authorize localhost.
  </Step>

  <Step title="2. Identify the action in the browser">
    The finished `public/index.html` loads the locally served SDK and public configuration. `public/shieldlabs.js` starts the check when the login form receives focus and queues checks. In `public/index.js`, take the fresh request ID and send it with the action:

    ```js theme={null}
    const requestId = await identification.take();
    const data = await postJson('/api/login', { email, password, requestId });
    ```

    The browser sends only the request ID. It does not send a Risk Score or the Private API Key.
  </Step>

  <Step title="3. Verify it on the server">
    `server/shieldlabs.js` reads History through `@shieldlabs-ai/node` using that exact request ID and the Private API Key. `server/server.js` passes the ID to `server/accounts.js`, where `verifyIdentification(requestId)` refuses missing, stale, reused, automated or unusable checks before the business rule runs.
  </Step>

  <Step title="4. Apply the account-takeover decision">
    In [`server/accounts.js`](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-takeover/server/accounts.js), the scenario uses the verified Device ID:

    ```js theme={null}
    if (knownDevices.length === 0 || knownDevices.some((device) => device.device_id === deviceId)) {
      rememberDevice(account.email, deviceId, publicIp.country);
      return startSession(account.email);
    }

    // The right password from an unknown device: ask for a one-time code.
    return startChallenge(account.email, deviceId, publicIp.country);
    ```

    The account's first verified device signs in directly. A different Device ID requires the demo's extra code, even when the password is correct.
  </Step>

  <Step title="5. Run the completed application">
    From the repository root, compare the files and switch to the version with the integration:

    ```sh theme={null}
    cd ..
    git diff starter origin/final -- account-takeover
    git switch final
    cd account-takeover
    npm ci --omit=dev
    npm start
    ```

    `origin/final` is available immediately after a fresh clone; your local `final` branch is created by `git switch final`. Open the completed app through your registered HTTPS hostname. Its server listens on `127.0.0.1:3000` behind your reverse proxy.
  </Step>
</Steps>

## Follow the integration code

The completed source is in the [account-takeover application](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/final/account-takeover). Read these files in order:

1. [Browser helper](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-takeover/public/shieldlabs.js): loads the installed SDK, serializes checks and returns a request ID.
2. [Server verification](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-takeover/server/shieldlabs.js): reads real History, checks the matching ID, rereads delayed results and rejects unusable or replayed checks.
3. [Scenario decision](https://github.com/ShieldLabs-ai/use-case-tutorials/blob/final/account-takeover/server/accounts.js): applies this app's device or account rule and stores its teaching state.

The [finished application](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/final/account-takeover) keeps its browser, server and SQLite state inside this folder. Follow the linked files above if you are adapting the idea to your own application.

## Try the completed application

1. Sign in as [demo@example.com](mailto:demo@example.com) with demo-password on one device. The first verified device becomes known to this sample account.
2. From a separately observed device, use the same credentials. The app should request its displayed six-digit demo code.
3. Enter that code to finish the sign-in. It is shown inside the demo; the app does not send email.

Use **Reset demo DB** with `DEMO_ALLOW_RESET=1` to repeat the exercise. These controls are for a disposable demonstration, not production authorization.

## Check your result

In DevTools **Network**, open the action's `/api/login` request and copy its `requestId`. Find that ID in History and compare the Device ID with the decision shown in the app. Compare the two Device IDs in History. The second one should become known to the demo account only after the code is accepted. Run `npm run check` and `npm test` from `account-takeover` to exercise the local behavior without making another identification.

For a real run, obtain fresh request IDs in the browser and confirm their Device IDs and signals in History. If identification returns HTTP 429, stop and check your account limits before retrying. A cookie change does not prove that the Device ID changed: compare the History rows.

## Adapt it to your product

This example is a starting point, not a production-ready authorization system. Bind each action to an authenticated session where appropriate, authorize administration screens, persist state and atomically consume request IDs across all server instances. Review legitimate shared-device behavior before applying a device-based restriction. See [Acting on results](/guides/acting-on-risk-score).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.