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. OWAPS 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.
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 customer trust can be destroyed by a cyber attack.
Businesses are being increasingly held accountable by the public and regulators to ensure their user’s 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 are lacking in 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.”
FingerprintJS 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, FingerprintJS Pro also provides tools for validating these identifiers sent by your front end. As a result, you will protect your users and your business against Credential Stuffing and other account takeover attacks with the proposed approaches. At the same time, your legit 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.
To use FingerprintJS 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 both the logic used to determine suspicious activity, as well as the challenge actions that should be taken when a visitor is flagged.
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:
// 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 result, get and store it.
// Typically on page load or on button click.
fpPromise
.then(fp => fp.get())
.then(fpResult => {result = fpResult})
The endpoint property is quite important and is used for the custom subdomain setup. Using a subdomain is required for correct identification while using Fingerprint Pro.
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, you can use FingerprintJS Server API Node.js SDK.
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 (!isRequestIdFormatValid || !isVisitorIdFormatValid) {
reportSuspiciousActivity(req);
persistUnsuccessfulLoginAttempt();
return getForbiddenReponse();
}
const fingerprintJSProServerApiUrl = new URL(
`https://api.fpjs.io/visitors/${visitorId}`
);
fingerprintJSProServerApiUrl.searchParams.append('request_id', requestId);
const visitorServerApiResponse = await fetch(
fingerprintJSProServerApiUrl.href, { method: 'GET', headers: { 'Auth-API-Key': 'secret-api-key' } }
);
// If there's something wrong with provided data, Server API might return non 200 response.
// We consider these data unreliable.
if (visitorServerApiResponse.status !== 200) {
persistUnsuccessfulLoginAttempt();
// Handle error internaly, refuse login.
}
const visitorData = await visitorServerApiResponse.json();
return visitorData;
if (visitorData.error || visitorData.visits.length !== 1) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
requestId
and visitorId
via phishing. It's recommended to check the freshness of the identification request to prevent replay attacks.if (new Date().getTime() - visitorData.visits[0].timestamp > 3000) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
if (visitorData.visits[0].confidence.score < 0.95) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponseAndChallenge();
}
// This is an example of obtaining the client 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();
}
const ourOrigins = [
'https://protect-login.example.com',
];
const visitorDataOrigin = new URL(visitorData.visits[0].url).origin;
if (
(visitorDataOrigin !== request.headers['origin'] ||
!ourOrigins.includes(visitorDataOrigin) ||
!ourOrigins.includes(request.headers['origin']))
) {
persistUnsuccessfulLoginAttempt();
reportSuspiciousActivity(req);
return getForbiddenReponse();
}
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.// Gets all unsuccessful login attempts during the last 24 hours or the visitorId.
const visitorLoginAttemptCountQueryResult = await db.query("SELECT COUNT(*) AS count FROM login_attempts WHERE visitor_id = ? AND timestamp > ? AND login_attempt_result NOT IN (?, ?, ?)", [visitorId, new Date().getTime() - 24 * 60 * 1000, "Passed", "TooManyLoginAttempts", "Challenged"]);
// If the visitorId performed 5 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();
}
visitorId
before. If not, we recommend using an additional way of verification, e.g. 2FA or email.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);
}
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 the suspicious activity via email/SMS/phone, or challenging the attempted login with two-factor authentication.
We have built a Credential Stuffing prevention demo to demonstrate the above concepts. Use this demo to see how you can use Fingerprint Pro in conjunction with simple logic rules to protect a login form. If you want to explore code, check our interactive Stackblitz demo or open-source GitHub repository. If you have any questions, please feel free to reach out to support@fingerprintjs.com.
Fingerprint’ open source technology is supported by contributing developers across the globe. Stay up to date on our latest technical use cases, integrations and updates.