When a hacker executes an account takeover (ATO), their main goal is to gain control of an account and exploit it for profit. For SaaS platforms this is dangerous for three reasons:
High-Value Targets and Rich Data: SaaS platforms act as central repositories for sensitive customer data, financial records, and intellectual property.
Difficulty in Detection: Once attackers are inside a SaaS platform, their actions often look like normal employee behaviour. They download files, share documents or even send messages.
Monetization Opportunities: Stolen SaaS accounts are valuable for stealing money, accessing financial apps, selling access on the black market, and launching further scams.
Traditional defenses like IP blocking fall short because attackers rotate IPs constantly. This guide shows you how to use Fingerprint's device intelligence to identify suspicious login attempts at the device level before access is granted.
Prerequisites
Fingerprint account (free tier works)
A basic login form in any frontend framework
How Fingerprint Identifies Suspicious Logins
Fingerprint generates a stable visitor ID by creating a unique signature based on a device's inherent hardware and software configurations, rather than relying on stored data like cookies. This identifier is persistent because it relies on the machine's actual characteristics. This makes it difficult to change, allowing it to bypass incognito mode, cookie deletion, and VPN-based IP masking.
For every identification request, a confidence score and suspect score is computed internally. The confidence score indicates the probability of a false-positive identification. It is a floating-point number whose value ranges from 0 to 1. The higher the value, the more confident Fingerprint is about not misidentifying this browser as another.
You can use the confidence score to request additional verification. For example, request OTP for browsers whose confidence score is below 0.95.
Suspect Score is a single value representing how many Smart Signals indicative of suspicious or fraudulent activity were triggered for a particular Event (identified by an event_id). Each triggered Smart Signal is represented by a positive integer called a weight. Weights are additive, meaning that the more Smart Signals return a positive value, the higher the final score.
Suspect Score can be used to flag and review possible suspicious activity. It can be used to quickly integrate Smart Signals into your fraud protection workflow without the need to explore fraud correlations of individual Smart Signals.
Installing the Fingerprint Javascript Agent
The Fingerprint JavaScript agent is a high-performance JavaScript function that collects multiple device and browser signals and sends them to the Fingerprint API for visitor identification and device intelligence analysis.
There are several options on how the FingerPrint Javascript Agent can be installed. These are:
- Import the agent directly from a CDN (fastest for a quick test).
- Install the agent loader using NPM (includes TypeScript support).
- Use a frontend SDK for your specific framework like React, Vue, Svelte, or Angular (recommended, includes built-in caching strategies and framework-specific APIs).
In this guide we will use the Vue SDK framework as an example.
Installation Guide
- Get API Key from your dashboard and add to your .env file.
VITE_FINGERPRINT_API_KEY=your_public_api_key_here
- Install the fingerprint dependency into the root of your project. This can be installed via npm or yarn.
npm install @fingerprintjs/fingerprintjs-pro-vue-v3
yarn add @fingerprintjs/fingerprintjs-pro-vue-v3
- Register the plugin in your Vue.js application. This snippet can be found on Fingerprint under the Vue Integration.
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import {
fpjsPlugin
} from '@fingerprintjs/fingerprintjs-pro-vue-v3';
const app = createApp(App);
app
.use(fpjsPlugin, {
loadOptions: {
apiKey: import.meta.env.VITE_FINGERPRINT_API_KEY
},
})
.mount('#app');
- Use the
useVisitorDatahook in your components to identify visitors. This is a sample of how it can be implemented.
<script setup lang="ts">
import { ref } from 'vue';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-vue-v3';
import { useRouter } from 'vue-router';
const email = ref('');
const password = ref('');
const isLoading = ref(false);
const error = ref('');
const fpResult = ref<null | {
visitorId: string;
requestId: string;
confidence: number;
suspectScore: number;
}>(null);
const router = useRouter();
const { getData } = useVisitorData(
{ extendedResult: true },
{ immediate: false }
);
function getRiskLevel(confidence: number, suspectScore: number) {
if (suspectScore >= 2 || confidence < 0.85) return 'high';
if (suspectScore === 1 || confidence < 0.95) return 'medium';
return 'low';
}
async function handleLogin() {
if (!email.value || !password.value) return;
isLoading.value = true;
error.value = '';
fpResult.value = null;
try {
// Always get a fresh result on login — never use cached data for security checks
const fpData = await getData({ ignoreCache: true });
if (!fpData) throw new Error('Fingerprint identification failed');
const { visitorId, requestId, confidence, suspectScore } = fpData as any;
fpResult.value = {
visitorId,
requestId,
confidence: confidence?.score ?? 0,
suspectScore: suspectScore ?? 0,
};
// Send credentials + Fingerprint data to your backend
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email.value,
password: password.value,
visitorId,
requestId,
}),
});
const result = await response.json();
if (!response.ok) {
error.value = result.message || 'Login failed';
return;
}
if (result.requiresOtp) {
router.push('/verify-otp');
return;
}
router.push('/dashboard');
} catch (err) {
error.value = 'Something went wrong. Please try again.';
} finally {
isLoading.value = false;
}
}
</script>
<template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div class="w-full max-w-md">
<!-- Card -->
<div class="bg-white border border-gray-200 rounded-2xl p-8 shadow-sm">
<!-- Logo / Header -->
<div class="flex items-center gap-3 mb-8">
<div class="w-9 h-9 rounded-lg bg-indigo-600 flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L4 6v6c0 5.25 3.5 10.15 8 11.35C16.5 22.15 20 17.25 20 12V6L12 2z" fill="currentColor" opacity="0.9"/>
<path d="M9 12l2 2 4-4" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-900 leading-tight">Secure sign in</p>
<p class="text-xs text-gray-400 leading-tight mt-0.5">Protected by Fingerprint</p>
</div>
</div>
<!-- Form -->
<form @submit.prevent="handleLogin" class="space-y-5">
<!-- Email -->
<div class="space-y-1.5">
<label for="email" class="block text-sm text-gray-500">
Email address
</label>
<input
id="email"
v-model="email"
type="email"
autocomplete="email"
placeholder="you@example.com"
required
class="w-full h-10 px-3 text-sm bg-gray-50 border border-gray-200 rounded-lg text-gray-900 placeholder-gray-300 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100"
/>
</div>
<!-- Password -->
<div class="space-y-1.5">
<label for="password" class="block text-sm text-gray-500">
Password
</label>
<input
id="password"
v-model="password"
type="password"
autocomplete="current-password"
placeholder="••••••••"
required
class="w-full h-10 px-3 text-sm bg-gray-50 border border-gray-200 rounded-lg text-gray-900 placeholder-gray-300 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100"
/>
</div>
<!-- Error message -->
<p v-if="error" class="text-xs text-red-500 bg-red-50 border border-red-100 rounded-lg px-3 py-2">
{{ error }}
</p>
<!-- Submit -->
<button
type="submit"
:disabled="isLoading"
class="w-full h-10 text-sm font-medium rounded-lg transition active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
:class="isLoading
? 'bg-indigo-400 text-white cursor-not-allowed'
: 'bg-indigo-600 hover:bg-indigo-700 text-white'"
>
<span v-if="isLoading" class="flex items-center justify-center gap-2">
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/>
</svg>
Verifying device...
</span>
<span v-else>Sign in</span>
</button>
</form>
<!-- Fingerprint result panel -->
<div
v-if="fpResult"
class="mt-5 rounded-lg border px-4 py-3 text-xs space-y-2"
:class="{
'bg-green-50 border-green-100': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'low',
'bg-yellow-50 border-yellow-100': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'medium',
'bg-red-50 border-red-100': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'high',
}"
>
<!-- Risk badge -->
<div class="flex items-center justify-between mb-1">
<span class="font-medium text-gray-700">Fingerprint result</span>
<span
class="text-xs font-medium px-2 py-0.5 rounded-full"
:class="{
'bg-green-100 text-green-700': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'low',
'bg-yellow-100 text-yellow-700': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'medium',
'bg-red-100 text-red-700': getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'high',
}"
>
{{
getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'low' ? 'clear' :
getRiskLevel(fpResult.confidence, fpResult.suspectScore) === 'medium' ? 'review' :
'high risk'
}}
</span>
</div>
<!-- Signal rows -->
<div class="divide-y divide-gray-100 border-t border-gray-100 pt-2 space-y-1.5">
<div class="flex justify-between pt-1">
<span class="text-gray-400">visitorId</span>
<span class="font-mono text-gray-700 truncate max-w-[55%]">{{ fpResult.visitorId }}</span>
</div>
<div class="flex justify-between pt-1">
<span class="text-gray-400">requestId</span>
<span class="font-mono text-gray-700 truncate max-w-[55%]">{{ fpResult.requestId }}</span>
</div>
<div class="flex justify-between pt-1">
<span class="text-gray-400">confidence score</span>
<span class="font-mono text-gray-700">{{ fpResult.confidence.toFixed(2) }}</span>
</div>
<div class="flex justify-between pt-1">
<span class="text-gray-400">suspect score</span>
<span class="font-mono text-gray-700">{{ fpResult.suspectScore }}</span>
</div>
</div>
<p class="text-gray-400 pt-1">
In production, <code class="text-gray-500">visitorId</code> and <code class="text-gray-500">requestId</code>
are sent to your Firebase Cloud Function for server-side Smart Signal verification.
</p>
</div>
<!-- Footer note -->
<p class="text-xs text-gray-300 text-center mt-6">
Fingerprint collects device signals on submission
</p>
</div>
</div>
</div>
</template>
Server-Side Verification with Smart Signals
Verification should be done server-side with Smart Signals fingerprinting primarily to ensure the security, integrity, and accuracy of fraud detection data, as client-side environments (browsers/mobile apps) are easily tampered with by malicious actors.
After your Vue component collects the visitorId and requestId from Fingerprint, those two values need to be sent to a backend for verification. This step is not optional as Smart Signals are deliberately unavailable in the client-side response to prevent tampering.
The flow looks like this:
Vue form collects
vistorIdandrequestIdon submission.Both values are sent to your backend along with the user's credentials.
Your backend calls the Fingerprint Server API using
requestIdto retrieve the full Smart Signals for that login events.Your backend evaluates the signals and decides whether to allow, challenge with OTP, or block.
Your backend returns the decision to the Vue application.
What each backend response means for your UI
200 OKwith norequiresOtpflag: Login is clean, redirect to dashboard.200 OKwithrequiresOtp: true: Fingerprint flagged the device as medium risk (VPN detected, low confidence score, or a single suspect signal). Route the user to an OTP screen before granting access. PassvisitorIdalong as a query param so your OTP handler can reference the same device context.401 Unauthorized: The backend detected high-risk signals (bot activity, browser tampering, blocklisted IP). Show an error and do not proceed. Do not tell the user exactly why they were blocked. A vague message is intentional.
Device fingerprinting is one layer in a broader account security strategy not a silver bullet. Pair it with rate limiting, multi-factor authentication, and anomaly detection to build a login flow that's genuinely difficult to compromise.
Top comments (0)