
Learn more about Fingerprint
- Streamline user experiences for trusted traffic
- The highest accuracy device identification for mobile and web
- Improve visitor analytics on mobile and web
What is Credential Stuffing?
Credential Stuffing is a method of account takeover where an attacker attempts to gain access to as many customer accounts as possible. Typically performed as an automated (brute force) attack, credential stuffing occurs when random combinations of usernames and passwords are submitted until a valid set is found and login is successful. OWASP notes that “credential stuffing is one of the most common techniques used to take over user accounts.”
Credential Stuffing attacks are effective because of the high probability of password reuse. Some studies suggest that as high as 85% of users will reuse the same credentials for multiple services. As long as password reuse is a common user behavior, Credential Stuffing will always be an attack vector of interest to bad actors.
Attack Stages:
- An attacker accesses lists of username and password pairs collected from data breaches.
- An attacker uses automated tools to test these username and password pairs on a web application.
- When a successful login happens, an attacker uses the credentials for profit. They may use the account to make fraudulent purchases, phishing attempts, online scams, sell private information, or sell login credentials.
Why is Credential Stuffing Prevention Important?
While the success of Credential Stuffing attacks is generally low in the one to three percent range, the negative effect of a successful attack can be tremendous. EY Global research found that a cyber attack can destroy customer trust.
Businesses are increasingly held accountable by the public and regulators to ensure their users’ data security and safety. It has become common for companies to sustain stiff fines and legal action under laws such as GDPR if their security standards, breach communication processes, and best practices lack compliance.
In 2018, the UK's Information Commissioner's Office (ICO) fined Uber £385,000 for "a series of avoidable data security flaws" exposing the data of approximately 2.7 million UK customers. In 2021, the French Data Protection Authority (CNIL) fined a data controller and its data processor €225,000 “for failure to implement adequate security measures to protect customer data against credential stuffing attacks on the website of the data controller.”
How to Prevent Credential Stuffing
Fingerprint Pro provides a unique identifier for every visitor to your website (the visitorId
) collected behind the scenes anytime someone visits a webpage with our JavaScript fingerprinting agent installed. Since malicious attackers might forge this data, Fingerprint Pro also provides tools for validating these identifiers sent by your front end. As a result, you can protect your users and your business against Credential Stuffing and other account takeover attacks with the approaches described below. At the same time, your legitimate users won’t experience any additional friction.
Since you know your product and business landscape best, it’s up to you to decide how to configure anti-fraud workflows to utilize the visitorId
to catch fraud on your website. Below, we have described some steps and best practices to use as a starting point for your custom solution.
Configuring Fingerprint Pro for Credential Stuffing prevention
To use Fingerprint Pro effectively to prevent all forms of account-related fraud, you should configure logic that utilizes the visitorId
, among other timestamped data, in conjunction with credentials provided by a user. It is crucial to think through the logic used to determine suspicious activity and the challenge actions that should be taken when a visitor is flagged.
Suspicious Activity Logic
We recommend that when a visitor attempts to log in, the visitorId
and login credentials are sent to your application server, where they persist in the storage layer. Using this data, you can compare the current visitorId
and credential pairing to previous attempts to catch threats.
Here are the recommended logic rules for Credential Stuffing:
First, add the Fingerprint Pro JavaScript agent to your webpage. Alternatively, if your frontend uses modern frameworks such as React.js or Angular, you can use one of our libraries instead.
// Initialize the agent.
const fpPromise = import('https://fpjscdn.net/v3/<your-public-api-key>')
.then(FingerprintJS => FingerprintJS.load({
endpoint: '<https://metrics.yourdomain.com>'
}));
// Once you need a fingerprinting result, get and store it.
// Typically, on page load or button click.
const fp = await fpPromise;
const result = await fp.get();
The endpoint property is important and is used for the custom subdomain setup. Using a subdomain is required for the most accurate identification while using Fingerprint Pro.
Send the user’s credentials with the visitorId
and requestId
to your authentication API.
// Send the user’s credentials together with `visitorId`
// and `requestId` to your authentication API.
const loginData = {
userName,
password,
visitorId: result.visitorId,
requestId: result.requestId,
};
const response = await fetch('/api/authenticate', {
method: 'POST',
body: JSON.stringify(loginData),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
All next steps should be performed on the backend. If your backend logic is built on top of Node.js or any other popular server-side framework or language, you can use one of our Fingerprint Server API SDKs.
const visitorId = req.body.visitorId;
const requestId = req.body.requestId;
const isRequestIdFormatValid = /^\d{13}\.[a-zA-Z0-9]{6}$/.test(requestId);
const isVisitorIdFormatValid = /^[a-zA-Z0-9]{20}$/.test(visitorId);
// If the `requestId` or `visitorId` is in the wrong format
// the request can be rejected immediately.
if (!isRequestIdFormatValid || !isVisitorIdFormatValid) {
reportSuspiciousActivity(req);
persistUnsuccessfulLoginAttempt();
return getForbiddenReponse();
}
All next steps and checks will be performed using data from the Fingerprint Pro Server API. Therefore, we need to obtain that visitor data first. Alternatively, one can use our Webhooks functionality.
const fingerprintJSProServerApiUrl = new URL(
`https://api.fpjs.io/visitors/${visitorId}`
);
fingerprintJSProServerApiUrl.searchParams.append('request_id', requestId);
const fingerprintOptions = {
method: "GET",
headers: { "Auth-API-Key": "<secret-api-key>" },
};
const visitorServerApiResponse = await fetch(
fingerprintJSProServerApiUrl.href,
fingerprintOptions
);
// If there's something wrong with the provided data,
// the Server API might return a non-200 response.
// We consider these data unreliable.
if (visitorServerApiResponse.status !== 200) {
persistUnsuccessfulLoginAttempt();
// Handle the error internally and refuse login.
}
const visitorData = await visitorServerApiResponse.json();
return visitorData;
The Server API response must contain information about this specific identification request. If not, the request might have been tampered with, and we don't trust this identification attempt.
// VisitorData must have the expected properties.
if (visitorData.error || visitorData.visits.length !== 1) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
An attacker might have acquired a valid requestId
and visitorId
via phishing. It's recommended to check the freshness of the identification request to prevent replay attacks.
// Fingerprinting event must be a maximum of 3 seconds old.
if (new Date().getTime() - visitorData.visits[0].timestamp > 3000) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
The Confidence Score reflects the system's degree of certainty that the visitor identifier is correct. If it's lower than a certain threshold, we recommend using an additional way of verification, e.g. 2FA or email.
// The Confidence Score must be of a certain level.
if (visitorData.visits[0].confidence.score < 0.95) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponseAndChallenge();
}
We want to check if the authentication request comes from the same IP address as the identification request.
// This is an example of obtaining the client's IP address.
// In most cases, it's a good idea to look for the
// right-most external IP address in the list to prevent spoofing.
if (
request.headers['x-forwarded-for'].split(',')[0] !==
visitorData.visits[0].ip
) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
Next, check if the authentication request comes from a known origin and if the authentication request's origin corresponds to the origin provided by the Fingerprint Pro Server API. One should also set the Request Filtering settings in the Fingerprint dashboard.
const ourOrigins = [
'https://protect-login.example.com',
];
const visitorDataOrigin = new URL(visitorData.visits[0].url).origin;
// Confirm that the authentication request is from a known origin.
if (
(visitorDataOrigin !== request.headers['origin'] ||
!ourOrigins.includes(visitorDataOrigin) ||
!ourOrigins.includes(request.headers['origin']))
) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
We also need to get all unsuccessful attempts during the last 24 hours for this given visitor. We won’t perform the login action if a visitor has reached the threshold. If the visitorId
had five unsuccessful login attempts during the last 24 hours, we do not perform the login. The count of attempts and time window might vary.
// Get the number of unsuccessful login attempts during
// the last 24 hours for the `visitorId`.
const loginAttemptQuery =
"SELECT COUNT(*) AS count FROM login_attempts WHERE visitor_id = ? AND timestamp > ? AND login_attempt_result NOT IN (?, ?, ?)";
const loginAttemptParams = [
visitorId,
new Date().getTime() - 24 * 60 * 60 * 1000, // 24 hours.
"Passed",
"TooManyLoginAttempts",
"Challenged",
];
const visitorLoginAttemptCountQueryResult = await db.query(
loginAttemptQuery,
loginAttemptParams
);
// If the `visitorId` performed five unsuccessful login
// attempts during the last 24 hours, we do not perform the login.
// The count of attempts and time window might vary.
if (visitorLoginAttemptCountQueryResult.count > 5) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
Finally, we can check if the provided credentials are correct. It’s also a good practice to check if they’ve ever logged in using the provided visitorId
before. If not, we recommend using an additional way of verification, e.g. 2FA or email.
// Check the user's credentials and if they're using a known `visitorId`.
if (areCredentialsCorrect(request.body.userName, request.body.password)) {
if (
isLoggingInFromKnownDevice(
visitorData.visitorId,
mockedUser.knownVisitorIds
)
) {
persistSuccessfulLoginAttempt();
return getOkReponse();
} else {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
} else {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
function isLoggingInFromKnownDevice(providedVisitorId, knownVisitorIds) {
return knownVisitorIds.includes(providedVisitorId);
}
Challenge Actions
You can require additional verification or authentication steps to stop fraudsters from further access for suspicious login attempts as defined by your suspicious activity logic.
In all the cases above, we suggest ignoring login attempts, notifying account owners about suspicious activity via email/SMS/phone, or challenging the attempted login with two-factor authentication.
Explore our Credential Stuffing prevention demo
We have built a Credential Stuffing prevention demo to demonstrate the above concepts. Use this demo to see how to use Fingerprint Pro and simple logic rules to protect a login form. If you want to explore the code, check our interactive Stackblitz demo or open-source GitHub repository. If you have any questions, please feel free to reach out to our support team.