how does it work and what is the difficulty?

Fingerprint in a nutshell

Fingerprint is a technology that allows you to identify clients based on the internal parameters of their browsers. It takes into account various data: information about the CPU, localization settings, audio, and so on.

Our team uses fingerprint to filter traffic and block bots. The accuracy of the determination can be estimated at demo.

How is a fingerprint token assigned?

We use a third party service with fingerprint functionality. There our traffic is filtered and then redirected to us.

The flow chart for assigning a token is something like this:

  • The initial request to our service from the browser.

  • The request goes to the fingerprint input gateway, which determines whether the traffic needs to be passed on.

  • At the input gateway, the presence of a special key in the cookie is checked, and then a decision is made to issue the page.

const keyValid = validateKey(request.cookies.my_key)
if (keyValid) {
 return proxyToRealFrontend()
} else {
 return proxyToFingerprintDumpHtml()
}
  • The user receives special HTML with a built-in script that collects data. After this, a token is generated and written to cookies

const uniqKey = getUniqKey({
 cpu: navigator.hardwareConcurrency,
 // ...
})
document.cookie += `token=${uniqKey}`

To simplify understanding, the stage of assigning HTTP-only cookies and possible storage in a table in the cloud has been omitted.

The algorithm is executed only for clients for which the token does not exist.

The algorithm is executed only for clients for which the token does not exist.

What was the problem?

Our project uses special measures to protect the /api of the web service. The principle of operation is simple: all incoming requests to /api are checked for the presence of a special token in the cookie. If there is no token, access to the data is denied.

Let's consider two scenarios for working with our API:

  1. The case of a missing token. In this scenario, try opening link in incognito mode. There is no token in the cookie, so the server will return a 403 error: API access is denied.

  2. The case of the present token. To get a token, go to the main page MTS Travel. Then go to API. The issued token is stored in your cookies and allows you to access the API. A 200 response will be returned with the expected data.

Security measures also provide for a limited validity period of the token – 1 hour. If we leave everything as is, after an hour the token will become invalid. Then the client, instead of another excellent hotel in Anapa, will receive an ugly error page and an offer to update.

How to solve a problem

Let's list the stages we have overcome:

  • Denial, anger. We went through them pretty quickly.

  • Bargain. According to the fingerprint vendor, a solution to this problem could be periodic requests to refresh the token. This gives rise to a banal solution, which we implement in the code.

setInterval(() => {
 fetch('/api/ping')
}, 1000 * 60 * 20)
  • Depression. After enabling fingerprint, we begin to notice an increase in user errors. A common case is long-term inactivity on a page. This means that for some reason our trivial solution does not work, and we need to dig further.

What we will learn:

  1. In fact, requests as a general rule do not update the token. It is updated only in a certain case, if the request came in a special period shortly before the expiration of the previous token.

  2. The solution does not take into account the case when the device is turned off or does not have network access. For example, a user visits our website and starts choosing a hotel. Having lost consciousness from the beauty of the hotels and the size of the cashback, the client accidentally closes the laptop lid. When he wakes up a few hours later, he sees a broken website and is very upset. The solution to this problem can be either a forced reload of the page, or a pop-up modal asking you to reload the page. Either way it doesn't look good.


  • Adoption. The problems are now obvious and understandable. Once we get rid of the tears, we need to figure out how to solve them elegantly. The train of thought is this:

Reboot. It cannot be avoided due to the device being turned off incident. Let's launch it, at least in a controlled manner. We can easily make an invisible iframe that loads our page and reload it. Then the token will be updated and the user will not notice the negative effect.

   <iframe id="my-iframe" hidden src="https://travel.mts.ru"/>
   // ...
   getElementById('my-iframe').reload()

When to update our iframe? At first, the idea was to reload the page periodically – just like we were going to make a banal request. But in practice we find that this also does not have the same effect as a regular API request. Moreover, this does not take into account the case with the device turned off. Here we are tempted to catch errors at the fetch level. That is, we want to make something like this request function code and use it everywhere instead of the usual fetch.

const customFetch_USE_IT_OR_WILL_BE_FIRED = (...args) => {
 // оригинальны запрос за данными
 let fetchResult = await fetch.apply(this, args)
 // проверка статуса ответа
 if (fetchResult.status === 403) {
   // обновление страницы в iframe, а вместе с ней и токена
   await reloadIframe()
   // повторный запрос
   fetchResult = await fetch.apply(this, args)
 }
 return fetchResult
}

This code contains some not-so-obvious logic involving a repeated request using fetch. This is due to the fact that after updating the iframe, we already have a valid token, but there is still no data. If we return the first result of a query, the application will inevitably crash. We take this into account as well as the 403 error: in our case, it means that the api has not started processing data. Therefore, we make a repeated request with the same data.

Having done all this, we find that using nextjs as a framework imposes additional restrictions. So, nextjs, when moving to another page, requests data already on the client. This means that these requests will also encounter probable 403s. We cannot tell nextjs which fetch to use. Then the following problem will arise: the user opens the device after a long period of inactivity, goes to the hotel page, and this initiates a request for new client data. Bottom line: we are faced with 403 again.

It looks like we have no other option but the most unpleasant one – patching fetch. It looks something like this:

// сохранение оригинального fetch
const originalFetch = window.fetch;
function patchedFetch = (...args) {
 // оригинальны запрос за данными
 let fetchResult = await originalFetch.apply(this, args)
 // проверка статуса ответа
 if (fetchResult.status === 403) {
   // обновление страницы в iframe, а вместе с ней и токена
   await reloadIframe()
   // повторный запрос
   fetchResult = await fetch.apply(this, args)
 }
 return fetchResult
}

// проверка среды исполнение, пропатчить fetch мы должны именно на клиенте
if (typeof window !== 'undefined') {
 window.fetch = patchedFetch
}
Final implementation

Final implementation

What can be improved

The solution above works, but it can of course be optimized. One such optimization could be to avoid simultaneous iframe updates from different sources. For example, after activating a device, the page can send several requests at once. They will all fail with a 403 error, so there will be multiple duplicate iframe updates.

To avoid this, we can introduce a global iframe update flag. And if the update is already running, just wait and not launch a new one.

let currentPromise = Promise.resolve();
let runningNow = false;

// ...

if (fetchResult.status === 403) {
   if (runningNow === false) {
     // инициализируем обновление iframe
     currentPromise = // начать обновление iframe и подписаться на его обновление внутри promise
     runningNow = true;
     await currentPromise;
     runningNow = false;
   } else {
     // если обновление уже запущено просто дожидаемся его
     await currentPromise
   }
 }

Bottom line

In this implementation, the token is updated without obvious problems and negative effect for the user. Of course, for this purpose we had to patch fetch. But for now this seems to be the only reasonable solution to solve this non-trivial problem.

This is how the case turned out. Share in the comments, have you implemented fingerprinting yourself, have you encountered the same problem, how did you solve it?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *