How to Integrate Fingerprinting Into Your Node Application

How to Integrate Fingerprinting Into Your Node Application

Browser fingerprinting is a technique that involves collecting varied information about someone's browser or device—such as device model, operating system, browser type and version, screen resolution, and CPU specifications—and combining them to create a unique digital identifier called a fingerprint.

Websites can use this fingerprint to identify visitors, which is useful for fraud detection and prevention. For example, think about someone logging onto your bank account. With browser fingerprinting (and your credentials), the website can determine that this isn't the browser that you typically use and then offer two-factor authentication or an OTP to ensure that it's indeed you trying to log in from a different device.

In this article, you'll learn how to use Fingerprint Pro to generate unique identifiers for your site's visitors using Node.js, an open-source, cross-platform JavaScript runtime environment. You will implement a user registration system that limits the number of signups from the same browser in a certain time range. Fingerprint Pro will help identify the user’s browser.

Prerequisites

You'll need a few things to follow along:

Set Up Your Project

To start, create a project folder, open it in your terminal, and execute the following command to initialize a Node.js application:

npm init -y

Next, execute the following command to install all the required dependencies:

npm i express ejs sqlite3 sequelize bcrypt @fingerprintjs/fingerprintjs-pro-server-api
  • express lets you create a web application with Node.js.
  • ejs lets you implement JavaScript templates for the frontend.
  • sqlite3 lets you create and interact with an SQLite database.
  • sequelize lets you interact with the SQLite database using JavaScript.
  • bcrypt lets you hash passwords before storing them.
  • @fingerprintjs/fingerprintjs-pro-server-api lets you interact with the Fingerprint Server API.

Implement the Frontend

In the project root directory, create a new folder named views. Create a new file named register.ejs in this folder and add the code below to it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Register</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background-color: #f4f4f4;
      }

      .form-container {
        background-color: white;
        border-radius: 5px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        width: 24rem;
        padding: 2rem;
      }

      .form-group {
        margin-bottom: 15px;
        width: 100%;
      }

      input {
        width: 100%;
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 3px;
      }

      button {
        padding: 10px 20px;
        background-color: #007bff;
        color: #fff;
        border: none;
        border-radius: 3px;
        cursor: pointer;
      }

      button:disabled {
        background-color: #ccc;
        cursor: not-allowed;
      }

      .error {
        font-size: small;
        color: red;
      }
    </style>
  </head>
  <body>
    <div class="form-container">
      <h2>Register</h2>
      <% if (typeof errors !== 'undefined') { %> <%
      errors.forEach(function(error) { %>
      <p class="error"><%= error %></p>
      <% }); %> <% } %>
      <form action="/register" method="POST" style="width: 100%;">
        <div class="form-group">
          <label for="email">Email:</label>
          <input type="email" id="email" name="email" value="" required />
        </div>
        <div class="form-group">
          <label for="password">Password:</label>
          <input
            type="password"
            id="password"
            name="password"
            value=""
            required
          />
        </div>
        <input type="hidden" id="requestId" name="requestId" />
        <input type="hidden" id="visitorId" name="visitorId" />
        <div class="form-group">
          <button type="submit" id="submitBtn" disabled>Register</button>
        </div>
      </form>
    </div>

    <script>
      // Initialize the agent once at web application startup.
      // Alternatively initialize as early on the page as possible.
      const fpPromise = import("https://fpjscdn.net/v3/PUBLIC_API_KEY").then(
        (FingerprintJS) => FingerprintJS.load()
      );

      // Analyze the visitor when necessary.
      fpPromise
        .then((fp) => fp.get())
        .then(({ requestId, visitorId }) => {
          // Set the values for the hidden form elements
          document.getElementById("requestId").value = requestId;
          document.getElementById("visitorId").value = visitorId;

          // Enable the submit button
          document.getElementById("submitBtn").disabled = false;
        });
    </script>
  </body>
</html>

This code renders a simple registration form with two visible fields for the email and password and two hidden fields. The code has a script tag that contains code for initializing the Fingerprint JavaScript agent using the CDN.

Two important methods are used to initialize the agent: import() downloads the JavaScript agent, and load() returns a promise that resolves to an instance of the agent.

After an agent instance has been acquired, the get() method sends an identification request for the current site visitor to the Fingerprint API. This method returns an object with four values:

  • requestId is a unique identifier for every request made to the Fingerprint API. It can be used to request information about a specific identification request from the server API.
  • visitorId is the unique identifier for the site visitor's browser/device.
  • visitorFound indicates if the visitor has been identified globally across all Fingerprint apps before.
  • confidence is a number between 0 and 1 that represents the probability of accurate identification.

The code extracts the visitorId and the requestId from the returned object, and these IDs are then used to populate two hidden input fields in the HTML form.

Note: Remember to replace PUBLIC_API_KEY with your unique public API key, which you can get from the Fingerprint dashboard in App Settings > API keys.

Obtaining API keys

Next, create a file named dashboard.ejs in the views folder and add the code below to it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dashboard</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background-color: #f4f4f4;
      }

      .dashboard-container {
        background-color: #fff;
        padding: 20px;
        border-radius: 5px;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="dashboard-container">
      <h2>Account created successfully</h2>
      <h4>Welcome to the dashboard!</h4>
    </div>
  </body>
</html>

This code renders a simple HTML page that the site visitor will be navigated to once they create an account.

Implement the Server Logic

With the front end ready, you can move on to implementing the server logic.

First, create a new file named db/database.db in the project root folder. It will act as the SQLite database for this application.

Next, create a file named server.js in the project root folder and add the code below to it:

const express = require("express");
const sqlite3 = require("sqlite3").verbose();
const fingerprintJsServerApi = require("@fingerprintjs/fingerprintjs-pro-server-api");
const { Sequelize, Op } = require("sequelize");
const bcrypt = require("bcrypt");
const UserModel = require("./models/User");
const DeviceFingerprintModel = require("./models/DeviceFingerprint");

const app = express();
const PORT = 5000;

// Initialize the Fingerprint Server API client instance
const client = new fingerprintJsServerApi.FingerprintJsServerApiClient({
  apiKey: "SECRET_API_KEY",
  region: fingerprintJsServerApi.Region.Global,
});

// Initialize sequelize with SQLite database connection
const sequelize = new Sequelize({
  dialect: "sqlite",
  storage: "./db/database.db",
});

// Create model instances
const User = UserModel(sequelize);
const DeviceFingerprint = DeviceFingerprintModel(sequelize);

// Set the view engine to ejs
app.set("view engine", "ejs");

// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));

// Define routes
// Display the registration page
app.get("/register", (req, res) => {
  res.render("register");
});

// Display the dashboard
app.get("/dashboard", (req, res) => {
  res.render("dashboard");
});

// Function to start the server
function startServer() {
  app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
  });
}

(async () => {
  await sequelize.sync({ alter: true });

  console.log("Model synchronized successfully");

  startServer();
})();

This code sets up a web application using the Express framework with two routes to display the registration and dashboard pages. Additionally, it connects to the SQLite database using the Sequelize ORM. It also initializes the Fingerprint Server API client instance that you will use to interact with the Fingerprint Server API. The server is started once the database models are synchronized. You will create the User model in the next step.

In the client initialization, the region is set to fingerprintJsServerApi.Region.Global, which means your Fingerprint application is deployed in the Global region. There are three regions where you can deploy your Fingerprint app:

Region Base URL Server Location
Global https://api.fpjs.io Global
EU https://eu.api.fpjs.io Frankfurt, Germany
Asia (Mumbai) https://ap.api.fpjs.io Mumbai, India

Make sure you update the code to match your app's region, which you set up when creating your Fingerprint application.

Again, remember to replace SECRET_API_KEY with the value of your key, which you can obtain from the Fingerprint Pro dashboard in App Settings > API keys > CREATE SECRET KEY.

Obtaining API keys

In the Create new API key dialog box, provide a name for your key (eg Test Key) and select Secret as the type of key.

Providing secret key details

To define the User model that you imported in the previous step, create a new file named models/User.js in the project root folder and add the code below to it.

const { DataTypes } = require("sequelize");

module.exports = (sequelize) => {
  const User = sequelize.define("User", {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
    },
    email: {
      type: DataTypes.TEXT,
      allowNull: false,
      unique: true,
    },
    password: {
      type: DataTypes.TEXT,
      allowNull: false,
    },
  });

  return User;
};

This code exports a function that defines a Sequelize model named User with the relevant fields: id, email, and password. The email field is set to be unique, which means that only one email address can be associated with a user. Finally, the function returns the User model definition.

You also need to create the DeviceFingerprint model that you imported earlier. To do this, create a new file named models/DeviceFingerprint.js in the project root folder and add the code below to it.

const { DataTypes } = require("sequelize");

module.exports = (sequelize) => {
  const DeviceFingerprint = sequelize.define("DeviceFingerprint", {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
    },
    fingerprint: {
      type: DataTypes.TEXT,
      allowNull: false,
    },
  });

  // Define association to User model
  // One device fingerprint can belong to multiple Users
  DeviceFingerprint.hasMany(sequelize.models.User, {
    foreignKey: "fingerprintId",
  });

  return DeviceFingerprint;
};

This code exports a function that defines a Sequelize model named DeviceFingerprint with the relevant fields: id and fingerprint. The fingerprint field stores the user’s device fingerprint. Additionally, it establishes an association with the User model, indicating that one device fingerprint can belong to multiple users.

Next, you need to add a route that handles the registration process. In this route, you will perform some additional validations to ensure that the fingerprint generated on the front end is reliable.

The Fingerprint Server API allows you to perform several validations to ensure the reliability of the generated device/browser fingerprint. In this tutorial, you'll only implement the following validations:

  • The visitor ID from the Fingerprint Server API must match the one in the request body. This helps prevent potential forgery attempts where the user might be trying to send a fake visitor ID.
  • The time between the identification timestamp and the request timestamp must be less than 120 seconds. (This value is only for illustrative purposes and can be changed to match your preferences.) A restraint like this reduces the likelihood of malicious actors intercepting and replaying identification data.
  • The user must not be a bot. Bots are often used to obtain unauthorized access, and this validation also helps with disruptive activities, such as scraping that results in overwhelming traffic.

To add these validations, add the following route to your code in the server.js file:

app.post("/register", async (req, res) => {
  const validateInput = async (requestId, visitorId) => {
    // Make a request to the Fingerprint Server API
    const eventData = await client.getEvent(requestId);

    // Define an errors array
    const errors = [];

    // Make sure the visitor ID from the server API matches the one in the request body
    if (eventData.products.identification.data.visitorId !== visitorId) {
      errors.push("Forged visitor ID");
    }

    // The time between the server identification timestamp and the request timestamp should be less than 120 seconds
    let timeDiff = Math.floor(
      (new Date().getTime() -
        eventData.products.identification.data.timestamp) /
        1000
    );
    if (timeDiff > 120) {
      errors.push("Forged request ID");
    }

    // Make sure the user is not a bot
    if (eventData.products.botd.data.bot.result === "bad") {
      errors.push("Bot detected");
    }

    return {
      errors,
      fingerprint: eventData.products.identification.data.visitorId,
    };
  };

  const createUser = async (email, password, fingerprint) => {
    try {
      // Check if the fingerprint already exists
      let existingFingerprint = await DeviceFingerprint.findOne({
        where: { fingerprint },
      });

      // If the fingerprint doesn't exist, create a new one
      if (!existingFingerprint) {
        existingFingerprint = await DeviceFingerprint.create({ fingerprint });
      }

      // Check if the fingerprint was added in the last 30 minutes
      const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); // 30 minutes ago

      const signupsWithinLastThirtyMinutes = await User.count({
        where: {
          fingerprintId: existingFingerprint.id, // Use the ID of the existing fingerprint
          createdAt: {
            [Op.gt]: thirtyMinutesAgo, // Check if signup timestamp is greater than thirty minutes ago
          },
        },
      });

      console.log("Number of signups", signupsWithinLastThirtyMinutes);

      // Check if more than a certain number of signups have occurred within the last 30 minutes
      const maxSignupsAllowed = 1; // Only one signup is allowed every 30 minutes
      if (signupsWithinLastThirtyMinutes >= maxSignupsAllowed) {
        // Handle the condition where more than the allowed number of signups occurred within the last 30 minutes
        return res.render("register", {
          errors: [
            "Unable to create account. Only one account can be created every 30 minutes.",
          ],
        });
      }

      // Hash the password
      let hashedPassword = await bcrypt.hash(password, 10);

      // Save the user with the existing fingerprint reference
      await User.create({
        email,
        password: hashedPassword,
        fingerprintId: existingFingerprint.id,
      });

      // Redirect the user to the dashboard upon successful registration
      res.redirect("/dashboard");
    } catch (error) {
      // Handle error appropriately
      if (error.name === "SequelizeUniqueConstraintError") {
        res.render("register", {
          errors: ["Looks like you already have an account. Please log in."],
        });
      } else {
        console.error("Error inserting user:", error);
      }
    }
  };

  // Extract request data
  const { email, password, requestId, visitorId } = req.body;

  // Perform validation
  const validation = await validateInput(requestId, visitorId);

  // Render appropriate errors
  if (validation.errors.length > 0) {
    return res.render("register", { errors: validation.errors });
  }

  // Create user
  createUser(email, password, validation.fingerprint);
});

The code above defines two functions: validateInput, which checks the validity of the provided user data by querying the Fingerprint Server API, and createUser, which attempts to create a new user in the database.

The validateInput function makes an asynchronous request to the Fingerprint Server API to retrieve event data associated with the provided requestId. It initializes an empty errors array to store any validation errors encountered during the validation process.

The function uses if statements to perform the validations mentioned before:

  • It checks if the visitorId from the server API response matches the one provided in the request body. If not, it indicates a potentially forged visitor ID and adds an error to the errors array.
  • It calculates the time difference between the visitor identification timestamp and the current time. If the difference exceeds 120 seconds, it adds an error indicating a potentially forged request ID to the errors array.
  • It checks if the Fingerprint Server API detects that the user is a bot. If so, it adds an error indicating bot detection to the errors array.

Note that the error handling logic implemented here is only for illustrative purposes to show the sample responses you can receive from the Fingerprint Server API. In a real-world application, you could include logic like showing a captcha if you detect that the user is a bot, prompting the user for two-factor authentication if the browser fingerprint doesn’t match the one in the database, and so on.

The validateInput function finally returns the errors and the device fingerprint.

The createUser function is responsible for creating a new user in the database. It first checks if the provided fingerprint exists in the database and creates it if it doesn’t. It then checks the number of signups associated with this fingerprint within the last thirty minutes using the User model’s count method, filtering by the fingerprintId and createdAt fields. If the count exceeds the maximum allowed signups(1) within the timeframe, it renders an error message. Otherwise, it creates a new user with a reference to the existing fingerprint and redirects the user to the dashboard.

Lastly, the route extracts the required info from the request object and calls the validateInput function to validate the data. If the validate function returns any errors, the register page is rendered with the appropriate errors. If no errors are returned, the createUser function is called with the appropriate data to add the new user to the database.

Test the Application

To test if your application is working as expected, execute the following command in the terminal to run the server:

node server.js

Navigate to http://localhost:5000/register on your browser, where you'll see the registration page:

Registration page

The Register button will be disabled while the Fingerprint Server API is generating a unique identifier for the current site visitor. Once the button is enabled, sign up and click it to be navigated to the dashboard page:

Note: If you have an ad blocker installed, the button may remain disabled if the ad blocker is blocking requests to the Fingerprint domains. In this case, you need to disable the ad blocker on this web page to test the application. If you want a more advanced solution for bypassing ad blockers, check out how to protect the Fingerprint JavaScript agent from ad blockers.

Dashboard page

If you navigate to http://localhost:5000/register and try to register an account with a different email address before thirty minutes are over, the account will not be created because you set up the server to allow creating only one account every thirty minutes from the same device:

Failed registration attempt

You can access the full code for this project here.

Conclusion

As you can tell from this tutorial, browser fingerprinting is invaluable in detecting and preventing online fraud.

For instance, if your site offers free trials for a service, you can use browser fingerprinting to limit the number of accounts created from a browser in a certain period. You can also block bots and suspicious users or apply extra authentication steps if they're detected to protect your site against cyberattacks.

Fingerprint Pro can identify users with industry-leading accuracy. Sign up for a free trial to test it for yourself.

All article tags

FAQ

What is browser fingerprinting?

Browser fingerprinting is a method to uniquely identify website users by analyzing details about their web browsers to generate an identifier. This information can include details like browser version, screen resolution, and installed fonts to uniquely identify a user's device.

What are the benefits of adding browser fingerprinting to your app?

Adding browser fingerprinting to your app can enhance security by detecting and preventing fraudulent activities. It can also be used to improve user experience by enabling personalized settings without requiring logins.

Why use browser fingerprinting over cookies?

Browser fingerprinting provides a much more persistent form of visitor identification than cookies, which users can easily clear or block. It allows for effective identification and fraud detection capabilities even with stricter privacy settings and cookie restrictions.

Share this post