Skip to main content

Overview

This tutorial walks through implementing Fingerprint to prevent account sharing, where multiple people use the same account across different devices. You’ll begin with a starter app that includes a mock login page and a basic login flow. From there, you’ll add the Fingerprint JavaScript agent to identify each visitor and use server-side logic with Fingerprint data to detect when an account is accessed from a new device. For simplicity, the sample will block logins from new devices for an account, but in practice, you would likely choose to add friction, log out other devices, or flag for review instead. By the end, you’ll have a sample app that limits an account to a single device and can be customized to fit your use case and business rules. This tutorial uses just plain JavaScript and a Node server with SQLite on the back end. For language- or framework-specific setups, see our quickstarts.
Estimated time: < 15 minutes

Prerequisites

Before you begin, make sure you have the following:
  • A copy of the starter repository (clone with Git or download as a ZIP)
  • Node.js (v20 or later) and npm installed
  • Your favorite code editor
  • Basic knowledge of JavaScript

1. Create a Fingerprint account and get your API keys

  1. Sign up for a free Fingerprint trial, or log in if you already have an account.
  2. After signing in, go to the API keys page in the dashboard.
  3. Save your public API key, which you’ll use to initialize the Fingerprint JavaScript agent.
  4. Create and securely store a secret API key for your server. Never expose it on the client side. You’ll use this key on the backend to retrieve full visitor information through the Fingerprint Server API.

2. Set up your project

  1. Clone or download the starter repository and open it in your editor:
Terminal
git clone https://github.com/fingerprintjs/use-case-tutorials.git
  1. This tutorial will be using the account-sharing folder. The project is organized as follows:
Project structure
.
├── public/
│   ├── index.html    # Login page with email/password
│   └── index.js      # Front-end logic to handle login
├── server/
│   ├── server.js     # Serves static files and login endpoint
│   ├── db.js         # Initializes SQLite and exports a database connection
│   └── accounts.js   # Account sharing logic
└── .env.example      # Example environment variables
  1. Install dependencies:
Terminal
npm install
  1. Copy or rename .env.example to .env, then add your Fingerprint API keys:
Terminal
FP_PUBLIC_API_KEY=your-public-key
FP_SECRET_API_KEY=your-secret-key
  1. Start the server:
Terminal
npm run dev
  1. Visit http://localhost:3000 to view the mock login page from the starter app. You can test the basic login form using the included test account (demo@example.com / password123) and clicking Log in. Then open the page in a completely different browser and log in with the same account. By default, the same account can be logged into from any device.

3. Add Fingerprint to the front end

In this step, you’ll load the Fingerprint client when the page loads and trigger identification when the user clicks Log in. The client returns both a visitorId and a requestId. Instead of relying on the visitorId from the browser, you’ll send the requestId to your server along with the login payload. The server will then call the Fingerprint Events API to securely retrieve the full identification details, including the verified visitorId and risk signals such as browser tampering or bot activity.
  1. At the top of public/index.js, load the Fingerprint JavaScript agent:
public/index.js
const fpPromise = import(
  `https://fpjscdn.net/v3/${window.FP_PUBLIC_API_KEY}`
).then((FingerprintJS) => FingerprintJS.load({ region: "us" }));
  1. Make sure to change region to match your workspace region (e.g., eu for Europe, ap for Asia, us for Global (default)).
  2. Near the bottom of public/index.js, the Log in button already has an event handler for submitting the credentials. Inside this handler, request visitor identification from Fingerprint using the get() method and include the returned requestId when sending the login request to the server:
public/index.js
loginBtn.addEventListener("click", async () => {
  // ...

  const fp = await fpPromise;
  const { requestId } = await fp.get();

  try {
    const res = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
	    body: JSON.stringify({ email, password, requestId }),
    });
    const data = await res.json();

    // ...
  }
});
The get() method sends signals collected from the browser to Fingerprint servers, where they are analyzed to identify the visitor. The returned requestId acts as a reference to this specific identification event, which your server can later use to fetch the full visitor details. For lower latency in production, check out our documentation on using Sealed Client Results to return full identification details as an encrypted payload from the get() method.

4. Receive and use the request ID to get visitor insights

Next, pass the requestId through to your login logic, initialize the Fingerprint Server API client, and fetch the full visitor identification event so you can access the trusted visitorId and Smart Signals.
  1. In the back end, the server/server.js file defines the API routes for the app. Update the /api/login route there to also extract requestId from the request body and pass it into the attemptLogin function:
server/server.js
app.post("/api/login", async (req, reply) => {
  const { email, password, requestId } = req.body || {};
  const result = await attemptLogin({ email, password, requestId });
  return reply.send(result);
});
  1. The server/accounts.js file contains the logic for handling logins. Start by importing and initializing the Fingerprint Server API client there, and load your environment variables with dotenv:
server/accounts.js
import { db } from "./db.js";
import { config } from "dotenv";
import {
  FingerprintJsServerApiClient,
  Region,
} from "@fingerprintjs/fingerprintjs-pro-server-api";

config();

const fpServerApiClient = new FingerprintJsServerApiClient({
  apiKey: process.env.FP_SECRET_API_KEY,
  region: Region.Global,
});
  1. Make sure to change region to match your workspace region (e.g., EU for Europe, AP for Asia, Global for Global (default)).
  2. Update the attemptLogin function to accept requestId and use it to fetch the full identification event details from Fingerprint:
server/accounts.js
export async function attemptLogin({ email, password, requestId }) {
  if (!email || !password) {
    console.error("Missing credentials.");
    return { success: false, error: "Login failed." };
  }

  if (!requestId) {
    console.error("Missing requestId.");
    return { success: false, error: "Login failed." };
  }

  const user = findAccountByEmail(email);
  if (!user || user.password !== password) {
    console.error("Invalid credentials");
    return { success: false, error: "Login failed." };
  }

  const event = await fpServerApiClient.getEvent(requestId);

  // ...
}
Using the requestId, the Fingerprint server client will retrieve the full data for the visitor identification request. The returned object will contain the visitor ID, IP address, device, and browser details, and Smart Signals like bot detection, browser tampering detection, VPN detection, and more. You can see a full example of the event structure and test it with your own device in our demo playground. For additional checks to ensure the validity of the data coming from your front end, view how to protect from client-side tampering and replay attacks in our documentation.

5. Block bots and suspicious devices

This optional step uses the Bot Detection and Suspect Score Smart Signals, which are only available on paid plans.
A simple but powerful way to prevent automated abuse is to block bot logins. The event object includes the Bot Detection Smart Signal that flags automated activity, making it easy to reject bot traffic. This signal returns good for known bots like search engines, bad for automation tools, headless browsers, or other signs of automation, and notDetected when no bot activity is found.
  1. Continuing in the attemptLogin function in server/accounts.js, check the bot signal returned in the event object:
server/accounts.js
export async function attemptLogin({ email, password, requestId }) {
  // ...

  const event = await fpServerApiClient.getEvent(requestId);

  const botDetected = event.products?.botd?.data?.bot?.result !== "notDetected";

  if (botDetected) {
    console.error("Bot detected.");
    return { success: false, error: "Login failed." };
  }

  // ...
}
You can also use Fingerprint’s Suspect Score to flag high-risk logins. The Suspect Score is a weighted representation of all Smart Signals present in the identification payload, helping to identify suspicious activity. While it’s not typical to block actions based solely on a high risk score, this example shows how you might incorporate it. In a real application, a better approach would be to flag the attempt for review or add some additional step-up authentication.
  1. Below the bot detection check, add a condition that reads the Suspect Score from the event object and blocks the login if it exceeds a chosen threshold (for example, 20):
server/accounts.js
export async function attemptLogin({ email, password, requestId }) {
  // ...

  const suspectScore = event.products?.suspectScore?.data?.result || 0;

  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    return { success: false, error: "Login failed." };
  }

  // ...
}

6. Prevent account sharing

Next, use the trusted visitorId from the event object to enforce a one-device-per-account rule. If the same account logs in from a different device (visitorId), reject the login. In production you may choose to allow a certain number of devices, add step-up verification, or alert the user. (This example oversimplifies login logic for demonstration purposes.) Note: The starter app includes a SQLite database with these tables already created for you:
SQLite database tables
accounts - Stores login credentials for test accounts
  email TEXT PRIMARY KEY
  password TEXT NOT NULL

account_devices - Logs which device (visitorId) is associated with each account
  email TEXT PRIMARY KEY
  visitorId TEXT NOT NULL
  createdAt INTEGER NOT NULL
  1. Add some helper functions to the bottom of the server/accounts.js file to read and write device details for an account:
server/accounts.js
// Get the last seen device for the user
function getLastDeviceFor(email) {
  return db
    .prepare(`SELECT visitorId FROM account_devices WHERE email = ?`)
    .get(email);
}

// Record the visitor's device on logging in
function saveDeviceFor(email, visitorId) {
  db.prepare(
    `INSERT OR IGNORE INTO account_devices (email, visitorId, createdAt)
     VALUES (?, ?, ?)`
  ).run(email, visitorId, Date.now());
}
  1. Update attemptLogin to retrieve the visitorId and use it to enforce the one device per account rule and record successful logins:
server/accounts.js
export async function attemptLogin({ email, password, requestId }) {
  // ...

  if (botDetected) {
    console.error("Bot detected.");
    return { success: false, error: "Login failed." };
  }

  const visitorId = event.products.identification.data.visitorId;

  const last = getLastDeviceFor(email);
  if (last && last.visitorId !== visitorId) {
    console.error("Access from new device detected.");
    return {
      success: false,
      error:
        "You can only access this account from one device. (Reset the demo to simulate logging out on the other device.)",
    };
  }
  if (!last) saveDeviceFor(email, visitorId);

  return { success: true };
}
This gives you a basic system to detect and block account sharing. You can extend it by allowing a small number of devices per account, expiring devices after some time, adding step-up verification on change, notifying the account owner, etc.
This is a minimal example to show how to implement Fingerprint. In a real application, make sure to implement proper security practices, error checking, and password handling that align with your production standards.

7. Test your implementation

Now that everything is wired up, you can test the full protected login flow.
  1. Start your server if it isn’t already running and open http://localhost:3000:
Terminal
npm run dev
  1. Log in (demo@example.com / password123) in one browser. You should see a success response.
  2. In a different browser or device, try logging in with the same account. The second attempt will be rejected because the account is tied to a different device.
  3. Bonus: Test the flow using a headless browser or automation tool to see bot detection in action. A sample script is available in test-bot.js. While your app is running, run the script with node test-bot.js in your terminal and observe that the automated logins are blocked.

Next steps

You now have a working login flow secured with Fingerprint. From here, you can expand the logic with more Smart Signals, fine-tune rules based on your business policies, or layer in additional checks for suspicious visitors. To dive deeper, explore our other use case tutorials for more step-by-step examples. Check out these related resources: