Strengthening IAM login security against account takeovers

Image for auth0 integration for ATO

Account takeover fraud isn’t just a security hiccup — it’s an earthquake shaking the foundations of businesses across industries. With attacks surging 354% year-over-year in 2023 and an average cost of $4.62 million per incident, companies face a threat that can devastate brand reputation and customer trust almost overnight. The impact goes far beyond immediate financial losses; high-profile breaches have led to stock plunges, legal battles, and mass client departures, underscoring the severe and far-reaching consequences of account takeover attacks.

Given these high stakes, forward-thinking businesses are beefing up their defenses by integrating advanced device intelligence solutions like Fingerprint with their existing authentication providers. This potent combination strengthens account protection and fraud prevention strategies by evaluating device risk and history during sensitive actions. In this tutorial, we’ll explore how Fingerprint can fortify your Identity and Access Management (IAM) system, using Auth0 as an example, to prevent account takeovers.

Understanding account takeover

Account takeover (ATO) occurs when attackers gain unauthorized access to user accounts, often to steal personal information and money or commit fraud. This can lead to significant financial losses and harm for the individual but also impacts the business by eroding customer trust and damaging its reputation.

Attackers use methods such as credential stuffing — where stolen usernames and passwords are used to access multiple accounts — and phishing, which tricks users into revealing their credentials. Social engineering manipulates individuals into taking actions they shouldn't, while brute-force attacks try various password combinations to get into accounts. Malware and session hijacking are also tools that allow attackers to capture credentials or take control of active sessions.

Relying solely on basic login methods like passwords can leave accounts vulnerable to increasingly sophisticated attacks. Account takeover methods can easily bypass standard defenses, including multi-factor authentication (MFA). Businesses need to implement additional layers of security, such as device intelligence along with MFA, to detect and respond to suspicious activity before it leads to a breach.

Introducing Fingerprint

Fingerprint is a device intelligence platform designed to identify online devices that interact with your website or mobile app, offering deep insights into their behavior. It creates a unique visitor identifier for each browser or device using a combination of browser and device attributes, maintaining consistent identification even if device settings are altered or cookies are cleared. In addition to this, Fingerprint's Smart Signals provide valuable information about potentially suspicious activities, such as VPN usage, browser tampering, or bot behavior.

With Fingerprint’s precise and persistent visitor IDs, businesses can reliably recognize returning users, detect fraudulent activity, and improve the user experience. By monitoring the patterns and behaviors associated with each visitor ID, businesses can swiftly detect anomalies like a login from an unfamiliar location or device and take action before any damage is done.

Enhancing IAMs with Fingerprint

IAM systems and authentication providers are vital for securing user access to customer-facing platforms. They ensure that only authorized users can access specific resources, protecting sensitive data and enhancing security. However, as threats like account takeovers evolve, there’s a growing need to strengthen these tools with additional security measures to better protect against online fraud.

Fingerprint’s device intelligence adds an extra layer of protection to your IAM processes, helping to prevent account takeovers and streamline authentication. Here are some of the ways Fingerprint can enhance your IAM:

Pre-authentication device intelligence

Fingerprint can evaluate a visitor's risk level before they even begin the authentication process. By determining if a device is recognized and trusted, Fingerprint enables you to adjust the login experience accordingly — either by adding friction, such as requiring MFA, or reducing it for known users. This ensures a smoother experience for legitimate users while creating obstacles for potential fraudsters.

Post-authentication analysis

After your IAM system authenticates the user, Fingerprint offers an additional layer of security by confirming that the authenticated session originates from the expected device. Cross-checking the visitor ID against a verified list of known IDs helps protect secure pages where session hijacking is a concern.

Enhanced security logs

Integrating Fingerprint’s highly accurate device intelligence with your IAM’s security logs provides more comprehensive insights into login attempts. This helps security teams better understand and respond to potential threats, offering a more informed approach to account protection.

Integrating Fingerprint with Auth0 to prevent account takeovers

To demonstrate how Fingerprint can be integrated with an IAM, we’ll use Auth0, a widely used authentication platform known for its ease of use and strong security features. Fingerprint can be seamlessly integrated with Auth0 to prevent account takeovers and other types of fraud. By identifying trusted or unfamiliar devices and using Smart Signals to detect suspicious behavior, you can reduce friction for legitimate users while blocking potential threats, all with minimal setup and easy integration into your application.

In the following step-by-step tutorial, we’ll cover how to implement pre-authentication checks using Fingerprint’s device intelligence. We’ll recognize visitor IDs and prompt for MFA when an unrecognized device attempts to log in. We’ll also show you how to use Fingerprint’s Smart Signals to detect and automatically block login attempts from bots. For this tutorial, we’ll be building on Auth0’s express sample application.

Requirements

Before you begin, make sure you have the following in place to follow along with this tutorial:

  1. Auth0 account: You’ll need an Auth0 account. If you don’t have one, you can sign up for a free account at Auth0's website.
  2. Fingerprint account: You’ll need a Fingerprint account to access the Fingerprint Pro features we’ll be integrating. You can sign up for a free, 14-day trial. You’ll need your public and secret API keys from the Fingerprint dashboard.
  3. Node installed: Ensure you have Node installed on your machine. You can download it from the official Node website. You might also want to install a tool like Nodemon to automatically apply changes while you follow along with the tutorial.
  4. Express sample application: Clone the Auth0 Express sample application found in Auth0's samples GitHub repository. Follow the instructions in the repository to set up the app on your local machine.

Once you have the sample application set up and running, you can proceed to the following steps to integrate Fingerprint.

Step 1: Request Fingerprint identification on login

To integrate Fingerprint with Auth0 for enhanced security, the first step is to request visitor identification using the Fingerprint JavaScript agent when the user clicks “Login.” The identifiers it returns will be used throughout the authentication process to help prevent account takeovers and detect suspicious activity.

Start by adding the agent to the views/index.ejs file in the Auth0 sample application in a new script block. The following script will initialize the Fingerprint agent:

<!-- views/index.ejs -->

<script>
  const fpPromise = import("https://fpjscdn.net/v3/PUBLIC_API_KEY").then(
    (FingerprintJS) => FingerprintJS.load()
  );
</script>

It’s best to load Fingerprint as soon as possible so it’s ready when you need it. Next, you'll want to make a handleLogin function to request visitor identification when “Login” is clicked. The identification request is made using fp.get(), which returns an object containing the requestId and visitorId. The requestId is unique to each identification and changes with every request, while the visitorId remains consistent and uniquely identifies the browser or device.

<!-- views/index.ejs -->

<script>
  const fpPromise = import("https://fpjscdn.net/v3/PUBLIC_API_KEY").then(
    (FingerprintJS) => FingerprintJS.load()
  );
  
  async function handleLogin() {
    const fp = await fpPromise;
    const { visitorId, requestId } = await fp.get();
  }
</script>

We want to include these IDs when we pass the user to the Auth0 login page. They will be used within our Auth0 logic later on. Redirect the user to the /start-login endpoint (we'll set up that route shortly.) Include in the query parameters the requestId, visitorId, and a returnTo parameter to specify where users should be redirected after logging in.

<!-- views/index.ejs -->

<script>
  const fpPromise = import("https://fpjscdn.net/v3/PUBLIC_API_KEY").then(
    (FingerprintJS) => FingerprintJS.load()
  );

  async function handleLogin() {
    const fp = await fpPromise;
    const { visitorId, requestId } = await fp.get();
    
    const currentUrl = encodeURIComponent(window.location.pathname);
    window.location.href = `/start-login?returnTo=${currentUrl}&visitorId=${visitorId}&requestId=${requestId}`;
  }
</script>

We need to incorporate this new function into the existing login link in the template. Update the anchor link pointing to /login to the following so that it will run our handleLogin function instead.

<!-- views/index.ejs -->

<a href="#" onclick="handleLogin()" class="underline">Login</a>

Step 2: Append the visitor ID to the login process

Now that we’ve got the frontend set up, let's add the /start-login route in the backend. You can find the routes for the sample application in the routes/index.js file. Here, we will add a new route to handle the login process:

// routes/index.js

router.get("/start-login", (req, res) => {
  const { visitorId, requestId, returnTo } = req.query;

  res.oidc.login({
    authorizationParams: {
      visitorId,
      requestId,
    },
    returnTo: returnTo || "/",
  });
});

In this route, we're extracting the parameters from the request query and passing the visitorId and requestId as additional authorization parameters for the login. This ensures they will be accessible later when we modify the login process in Auth0 and implement our visitor verification checks. We're also passing the returnTo parameter to redirect users to the appropriate page after they log in.

At this stage, you can test the login process. You should be able to log in without any issues, and in the next step, we'll begin using the values we've passed to Auth0.

If visitor identification is blocked during testing, make sure ad blockers are turned off. To ensure visitor identification works in production even with ad blockers, see our guide on protecting your JavaScript agent.

Step 3: Enable MFA with Actions in Auth0

In order to add our verification checks, we will use Auth0 Actions. Actions are customizable, serverless functions that execute at specific points during the authentication and authorization process. They allow you to extend and modify Auth0's default behavior by running your own custom code in response to events like user login or token issuance.

Our custom actions will carry out the following steps:

  • Verify that the visitor ID matches the identification event to ensure the visitor is not attempting to spoof the ID.
  • Detect if the visitor is a bot, and if so, deny their login attempt.
  • Determine if the visitor ID is associated with the account; if it's an unknown device, prompt for MFA. If it's recognized, proceed with the login process.
  • If all checks pass, add the visitor ID to the list of known devices for the account and complete the login process.

In order to prompt for MFA, you will first need to enable it in your Auth0 application. Start by logging in to your Auth0 account at auth0.com. In the Auth0 dashboard, navigate to Security on the left-hand menu, then select Multi-factor Auth. Choose one of the available MFA options, for this example we will use One-time Password (OTP). Make sure that the Require Multi-factor Auth policy is set to Never. Setting the policy to Never allows the actions we'll configure to determine whether to prompt for MFA or not. Additionally, make sure Customize MFA Factors using Actions is toggled on to enable MFA verification logic in our actions.

Step 4: Create the first action

To create a new action, in the Auth0 dashboard on the left-hand sidebar, click on Actions and then click Library. In the Library, click Create Action > Build from scratch to start a new custom action. Give the first action a meaningful name, like "Fingerprint Login Pt. 1" and choose Login / PostLogin as the trigger for the action.

You will then be presented with an editor where you can write your action. It should have something similar to below to start with:

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {

};

On the left, click Add Secret and add your Fingerprint secret key with the key FINGERPRINT_SECRET_API_KEY and your API key as the value. If you don’t see the button to add a new secret click on the key icon. Next, we want to add the Fingerprint Server API library as a dependency — click on the box icon and click Add Dependency. Enter @fingerprintjs/fingerprintjs-pro-server-api to include the Fingerprint Node SDK. Now we can start to flesh out the action script.

Step 4.1: Verify the visitor information

Before we start our checks, let’s first initialize the Fingerprint Server API library and pull the visitorId and requestId from the request query parameters that we sent.

/**
 * Handler that will be called during the execution of a PostLogin flow.
 *
 * @param {Event} event - Details about the user and the context in which they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
 */
exports.onExecutePostLogin = async (event, api) => {
  const {
    FingerprintJsServerApiClient,
    Region,
  } = require("@fingerprintjs/fingerprintjs-pro-server-api");

  const client = new FingerprintJsServerApiClient({
    region: Region.Global,
    apiKey: event.secrets.FINGERPRINT_SECRET_API_KEY,
  });

  // Extract the visitorId and requestId from the query parameters
  const { visitorId, requestId } = event.request.query;
  if (!visitorId || !requestId)
    return api.access.deny("Visitor identification not found.");

We can now verify whether the visitorId we received matches the one linked to the requestId in the identification event. This step ensures the visitor isn't attempting to spoof the visitor ID.

Using the requestId, retrieve the identification event from the Fingerprint Server API, which includes the actual visitor ID along with additional data about the visitor. If the two visitor IDs don't match, the login will be denied. You can also choose to not send the visitor ID from the frontend at all and only request it with the requestId in your action.

  // Fetch the identification event using the requestId
  const identificationEvent = await client.getEvent(requestId);
  const eventVisitorId =
    identificationEvent?.products?.identification?.data?.visitorId;

  // Confirm the visitorId is the same as the one in the identification event
  if (!eventVisitorId || visitorId !== eventVisitorId)
    return api.access.deny("Visitor identification error.");

We are going to want to use the visitor ID later on in a second action. A simple way to save and pass this information is to use Auth0's app metadata. Generally, it's recommended to use app_metadata over user_metadata for storing critical information since users can potentially edit their own user_metadata, making it unsuitable for secure or sensitive data.

  // Save the visitorId within the app metadata
  api.user.setAppMetadata("currentVisitorId", eventVisitorId);

Step 4.2: Check for bot activity

Next, we can check whether the visitor has been identified as a bot. If bot activity is detected, we deny the login attempt to protect against automated attacks. Fingerprint’s bot detection Smart Signal returns notDetected for humans, good for legitimate bots like well-known search engine crawlers, and bad for automated tools with potential fraudulent intent.

  // Get bot detection result
  const botDetection = identificationEvent.products?.botd?.data?.bot?.result;

  // If a bot is detected, deny the login
  if (botDetection !== "notDetected") return api.access.deny("Login denied.");

These are the only identification event checks we will do in this tutorial, but this is a good point to add additional checks such as checking the staleness of the identification request and the origin of the request. You can see examples of additional checks for the visitor identification in our SMS fraud tutorial.

In addition to bot detection, Fingerprint Smart Signals can tell if a visitor has tampered with their browser or device, is using a VPN, is on Tor, and a variety of other signals. Depending on your use case, these signals can point to suspicious activity and may require extra verification. You can develop your own custom logic in the Auth0 action using these signals or use our built-in Suspect Score for a quick way to assess suspicion.

Step 4.3: Ensure MFA is on for all users

Before sending an MFA challenge, the user needs to enroll in MFA. In our action, we can check if it's their first login and, if so, prompt them to enroll in MFA. Since it's their first time, we skip the unknown devices check, which will happen in the next step, for now.

  // If first login, enroll in MFA
  const enrolledMFAs = event?.user?.multifactor?.length;
  if (enrolledMFAs == 0 || event.stats.logins_count == 1)
    return api.authentication.enrollWith({ type: "otp" });

Step 4.4: Check for unknown devices

Now that we've verified the visitor identification data, blocked any bots, and made sure the user has enrolled in MFA on signup, we can shift our focus to the device recognition checks. Specifically, we want to determine whether a device has been used to successfully access the account before. If it hasn’t, we'll prompt for MFA for an additional layer of verification.

A simple way to link devices to accounts is by storing a list of visitor IDs in the Auth0 app metadata for the user. First, we check the user’s app_metadata property to see if the visitor ID is already associated with the account. If there are no visitor IDs listed or if the current visitor ID isn't among them, we then issue an MFA challenge.

  const appMetadata = event.user.app_metadata || {};

  // Check if the visitorId is already associated with the user
  // If not, prompt for MFA
  if (!appMetadata.visitorIds || !appMetadata.visitorIds.includes(visitorId)) {
    // Store the need for MFA verification in the app metadata
    // This will be used in the following action
    api.user.setAppMetadata("mfaNeeded", true);
    
    // Trigger a MFA challenge, here we are using OTP
    api.authentication.challengeWithAny([{ type: "otp" }]);
  }
};

Step 5: Create the second action

At this point, we want our login flow to pause while the user completes MFA if it is needed. By separating our flow into two actions, we are able to get information on whether or not the MFA was completed. Otherwise, the script would just continue without making this check.

Make a new action following the same instructions from the beginning of step 4. Call it something like "Fingerprint Login Pt. 2" to be able to easily recognize it.

Step 5.1: Retrieve the app metadata

First, get the helper data we stored in the app metadata for currentVisitorId and mfaNeeded. You can also clear these values after retrieving them as they will no longer be needed after this action.

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  // Get variables from first Action
  const appMetadata = event.user.app_metadata || {};
  const visitorId = appMetadata.currentVisitorId;
  const mfaNeeded = appMetadata.mfaNeeded;
  api.user.setAppMetadata("currentVisitorId", null);
  api.user.setAppMetadata("mfaNeeded", null);
  
  // If no visitorId has been passed there is an error
  if (!visitorId) return api.access.deny("Visitor identification not found.");

Step 5.2: Confirm authentication and MFA

If an MFA challenge was sent, we want to confirm that the user completed it. We can use event.authentication to see what authentication methods have been completed at this point. Remember, the flow of actions had paused for the user to complete the MFA challenge if it was requested. At this point, we have information on which authentication steps have been done. If the user did not succeed with the MFA challenge we can deny them access.

  // Check if the user successfully completed authentication and MFA if needed
  if (!event.authentication) return api.access.deny("Failed authentication.");
  const mfaSuccess = event.authentication.methods.find((m) => m.name == "mfa");
  if (mfaNeeded && !mfaSuccess) return api.access.deny("Failed multi-factor authentication.");

If the user is unable to successfully complete the MFA prompt, they won't proceed any further in the action. Therefore, everything that follows applies only to users who have either successfully verified through MFA or are using devices recognized from previous logins.

Step 5.3: Update the known devices list

Now, we'll add the visitor ID to the list of known visitor IDs (if it's not already included).

  // If successfully passed all checks, associate the visitor ID with the user
  let updatedVisitorIds = appMetadata.visitorIds || [];
  if (!updatedVisitorIds.includes(visitorId)) {
    // Update the app_metadata with the new visitorId
    updatedVisitorIds.push(visitorId);
    api.user.setAppMetadata("visitorIds", updatedVisitorIds);
  }

Optionally you can save the list of recognized visitor IDs as a custom claim so it can be accessed later on from your app.

  // Optional: set the visitorIds as a custom claim so it can be accessed from the app
  api.idToken.setCustomClaim("visitorIds", updatedVisitorIds);
};

That completes our second custom action, you can find the full scripts on GitHub. At this point you can log out and test the login process from the sample app. You will be prompted to for MFA since there are no visitor IDs associated with your account yet. Once you successfully log in, you will see a new visitorIds array as a part of the profile data displayed on the /profile page like so:

{
  "visitorIds": [
    "aBcwNUMeSVwJQclYI123"
  ],
	... other attributes
}

You've now successfully integrated Fingerprint Identification and Smart Signals into your Auth0 login flow! Legitimate users on recognized devices will have a seamless sign-in experience, while unknown or suspicious devices will be denied access or prompted for additional verification.

Secure your accounts against takeovers with Fingerprint

Account takeover is a serious threat that can wreak havoc on both businesses and users. While IAM systems like Auth0 are essential for managing and protecting user accounts, relying on them alone may not be enough to outsmart today’s attackers.

By adding Fingerprint to your IAM, you introduce a powerful layer of security that goes beyond traditional methods. Combining Fingerprint’s highly accurate visitor IDs, Smart Signals, and your IAM can enhance the accuracy of your authentication processes, reduce friction for trusted users, and create a more secure and user-friendly authentication experience.

Ready to protect your platform from account takeover fraud? Contact us today for expert assistance or start a free trial to experience the power of Fingerprint firsthand. You can view the complete version of this sample on GitHub.

Share this post