{"id":15412704,"url":"https://github.com/molefrog/fpjs-login-demo","last_synced_at":"2026-05-19T15:34:19.980Z","repository":{"id":105126851,"uuid":"541123390","full_name":"molefrog/fpjs-login-demo","owner":"molefrog","description":"FingerprintJS Pro Demo: preventing credential stuffing attacks","archived":false,"fork":false,"pushed_at":"2022-09-25T21:39:45.000Z","size":429,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-25T17:12:21.525Z","etag":null,"topics":["demo","fingerprinting-websites","fingerprintjs","tutorial"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/molefrog.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-25T09:45:06.000Z","updated_at":"2022-11-16T17:30:34.000Z","dependencies_parsed_at":null,"dependency_job_id":"f0de6235-b292-4d9a-9d27-082180fe3125","html_url":"https://github.com/molefrog/fpjs-login-demo","commit_stats":{"total_commits":21,"total_committers":1,"mean_commits":21.0,"dds":0.0,"last_synced_commit":"e6ca69f15129e0a551578e369bfc0e324450c2c5"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molefrog%2Ffpjs-login-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molefrog%2Ffpjs-login-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molefrog%2Ffpjs-login-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molefrog%2Ffpjs-login-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/molefrog","download_url":"https://codeload.github.com/molefrog/fpjs-login-demo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244669014,"owners_count":20490707,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["demo","fingerprinting-websites","fingerprintjs","tutorial"],"created_at":"2024-10-01T16:54:11.446Z","updated_at":"2026-05-19T15:34:09.948Z","avatar_url":"https://github.com/molefrog.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Preventing credential stuffing attacks\n\nIn this demo, we're going to explore how [FingerpringJS Pro](https://fingerprint.com/) can easily help you to protect your website or web service \nagainst automated [credential stuffing attacks](https://auth0.com/resources/whitepapers/credential-stuffing-attacks). To do so, we're going to \nbuild a simple login form experience with a server-side request throttling based on the current [`visitorId`](https://dev.fingerprint.com/docs/js-agent#visitorid).\n\n![The application we're going to build](https://github.com/molefrog/fpjs-login-demo/blob/main/static/app-screenshot.png?raw=true)\n\nWhy is this important? Almost every app on the Web starts with a login form. Although developers have being doing this for years, it's always hard \nto build bulletproof and secure login and session management in your apps. User credentials are [being leaked](https://haveibeenpwned.com/) all \nthe time, mature businesses are not the exception.\n\nWhat you'll learn in this tutorial:\n- How to build a simple login form with Remix and React\n- How to implement a basic login attempts logging\n- How to use Fingerprint's [React integration](https://github.com/fingerprintjs/fingerprintjs-pro-react) to get the current browser identifier\n- How to implement request throttling based on that identifier\n\n## 1. Getting Started\nLet's bootstrap our app! \n\nWe'll use [Remix](https://remix.run/) – a React framework that is currently getting a nice adoption in the \nweb community. Remix uses a slightly different approach from other isomorphic React toolkits utilizing a concept of loaders and actions. \nIt might sound a bit complicated, but please bear with me, it's going to be fun!\n\n```bash\n# let's bootstrap a basic Remix app\nnpx create-remix@latest login-app\n```\n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/106167647f78c06520dd83ccaef239ab1387e096)**\n\n## 2. Shaping Up Our Login Form\n\nIt's nice to start with some fake implementation first, so we can focus on the real logic later. While it's always fun to write CSS, for \nthe prototype, I recommend using something that doesn't require much configuration (like classeless CSS framework [new.css](https://newcss.net/)).\n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/3285d1153486f4fdd92176f7ce016e7ae1db9130)**\n\nRemix provides a handy component `Form` for form submission and `useTransition` hook that we'll use for a loader. \n\n```tsx\nimport { useTransition, Form } from \"@remix-run/react\";\n\nconst transition = useTransition();\n\n// Transitions in Remix represent the navigation state\n// That's how we know if the form is being sumbitted or not\nconst isLoading = transition.state !== \"idle\";\n\nreturn (\n  \u003cForm method=\"post\" action=\"/login\"\u003e\n    {/* form fields and loading indicator */}\n  \u003c/Form\u003e\n)\n```\n\nNext, let's implement fake form submission: it will only accept one hardcoded email and return an error otherwise.\n\n```tsx\nexport const action: ActionFunction = async ({ request }) =\u003e {\n  const data = await request.formData();\n\n  const username = data.get(\"email\") as string;\n\n  // login successful!\n  if (username === \"admin@example.com\") {\n    return redirect(\"/account\");\n  }\n\n  return json\u003cFormResponse\u003e({\n    errorMessage: \"Bad luck, please try different login or password!\"\n  });\n};\n```\n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/4e3edd4717c6409e321730a275d64b87a509c126)**\n\n## 3. Wiring It Up\n\nNow it's time to implement real authentication that will rely on email and password stored in a database. To do that, we'll \ncreate a SQLite database and provision it with some users. Let's initialize a new database file and create a `users` table. Tip: to speed things up, use a GUI client like SQLPro. \n\n```sql\nCREATE TABLE \"users\" (\n  \"id\" integer PRIMARY KEY NOT NULL,\n  \"email\" char(128) NOT NULL,\n  \"password\" char(128) NOT NULL,\n  \"username\" char(128) NOT NULL\n)\n```\n\n\u003e ⚠️ Warning: while in this particular example we use passwords as-is, you should never ever store passwords in plain text! \n\u003e Always rely on hashed and salted values instead. [Read this](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)\n\u003e for more information.\n\nNow when a request comes in, we just need to find if that email-password pair matches any \nrecord in our database:\n\n```tsx \n// routes/index.tsx\nimport { findUserByCredentials } from \"../db/queries.server\";\n\n// loader\nexport const action: ActionFunction = async ({ request }) =\u003e {\n  // ... extract email and password from FormData\n\n  const user = await findUserByCredentials(email, password);\n\n  if (!user) {\n    // no user found, respond with an error message\n    return json\u003cFormResponse\u003e({\n      errorMessage: \"Bad luck, please try different login or password!\"\n    });\n  }\n\n  // respond with a redirect\n}\n```\n\nYou might notice that the file we're importing `findUserByCredentials` from is called `queries.server.ts`. That is a special [naming convention\nRemix](https://remix.run/docs/en/v1/guides/constraints) used to exclude server-side code from the bundle when it can't automatically prune it\n(the library `sqlite` we're using can only be used within Node). \n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/408ab58842e84b38030a2d0ec6a6445ab40068e9)**\n\n## 4. Introducing Request Throttling\n\nFinally, let's ensure that our login form is protected against malicious bots trying to brute force credentials. We'll log all unsuccessful \nlogin attempts and then block the requests that happen suspiciously too often. Let's create a new table, `login_attempts`:\n\n```sql\nCREATE TABLE \"login_attempts\" (\n  \"id\" integer PRIMARY KEY NOT NULL,\n  \"visitor_id\" char(128),\n  \"created_at\" timestamp(128) NOT NULL,\n  \"email\" char(128) NOT NULL\n)\n```\n\nWe'll need some way to identify requests: obviously, we can't entirely rely on the email as well as on the IP (since there might be users who are sitting \nbehind a NAT) – and that's where `visitor_id` column comes in. We will soon see how to obtain that identifier but for now let's write a logging function.\n\n```tsx \n// call this method when authentication wasn't successful\nexport async function logFailedLoginAttempt(\n  email: string,\n  visitorId: string\n): Promise\u003cvoid\u003e {\n  const db = await openDB(); // open and init SQLite DB\n\n  await db.run(\n    \"INSERT INTO login_attempts (email, visitor_id, created_at) VALUES (?, ?, ?)\",\n    email,\n    visitorId,\n    Date.now()\n  );\n}\n```\n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/3548c72f84394fb98241c81095f79a2647916415)**\n\nIdentifying visitors could be tricky as most bots are already aware of standard methods such as cookies or IP-based identification. \nThat's exactly the problem FingerprintJS solves! It is an [open-source library](https://github.com/fingerprintjs/fingerprintjs/) for bullet-proof \ndevice identification and it has a Pro version with more advanced features such as persistent visitor IDs or backend-based fingerprinting.\n\nIn this demo, we're going to use an [official React library](https://github.com/fingerprintjs/fingerprintjs-pro-react) that can be integrated in just \nfew minutes. \n\nFirst of all, we will need to wrap our app in `FpjsProvider` to allow underlying components to use the library:\n\n```tsx\n// app/root.tsx\n{/* \n  By wrapping the \u003cOutlet /\u003e with \u003cFpjsProvider /\u003e we ensure that all routes\n  in our app can properly access Fingerprint's API\n*/}\n\u003cFpjsProvider loadOptions={{ apiKey: FPJS_API_KEY }}\u003e\n  \u003cOutlet /\u003e\n\u003c/FpjsProvider\u003e\n```\n\nRemix provides an `app/root.tsx` file where you can customize the app-wide layout. Now, in our login component, we're going to use the `useVisitorData` hook\nto obtain the visitor id. Note that this hook is asynchronous, and this is why it returns the `isLoading` flag.\n\n```tsx\n// routes/login.tsx\nimport { useVisitorData } from \"@fingerprintjs/fingerprintjs-pro-react\";\n\n// Get the current browser identifier\nconst { isLoading: isVisitorIdLoading, data: visitorData } = useVisitorData();\n\nconst visitorId = visitorData?.visitorId;\n```\n\nYou can also [pass `{ immediate: false }`](https://github.com/fingerprintjs/fingerprintjs-pro-react#getting-started) to the hook if you'd like \nto manually trigger the identification process. \n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/6e658467a339c7c025e22d42c20ce61af6c512cb)**\n\nThe remaining part is figuring out if we want to block current requests when the number of attempts exceeds the threshold. To do\nthat, we just have to write a simple query: it will get the number of attempts coming from this visitor within a fixed time interval:\n\n```tsx\nexport async function shouldThrottleLoginRequest(\n  visitorId: string,\n  options: ThrottlingOptions = { maxAttempts: 5, periodMins: 5 }\n): Promise\u003cboolean\u003e {\n  // when does the throttling window start\n  const startTime = Date.now() - options.periodMins * 60 * 1000;\n\n  // get the number of failed login attempts for the current visitor\n  const row = await db.get\u003c{ numAttempts: number }\u003e(\n    `SELECT count(*) as numAttempts FROM login_attempts \n      WHERE visitor_id = (?) AND created_at \u003e (?)`,\n    visitorId,\n    startTime\n  );\n\n  // threshold exceeded - block the request!\n  if (Number(row?.numAttempts) \u003e options.maxAttempts) {\n    return true;\n  }\n\n  return false;\n}\n```\n\n👉 **[Commit](https://github.com/molefrog/fpjs-login-demo/commit/b9bf0cff62a15a169f8fac4e70ef347777878d7c)**\n\nAnd that's it! Now our login form is protected against credential stuffing attacks. Obviously, there is a lot of things you can further improve in this \nimplementation, but I hope this tutorial helped you get a basic glimpse of how visitor-based request throttling works. \n\n## Further Reading\n- [Standford Web Security lectures](https://web.stanford.edu/class/cs253/) by [@feross](https://twitter.com/feross)\n- [FingerprintJS official documentation](https://dev.fingerprint.com/docs/pro-vs-open-source)\n\n## Running the code locally\nJust run `npm run dev` and you're good to go.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolefrog%2Ffpjs-login-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmolefrog%2Ffpjs-login-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolefrog%2Ffpjs-login-demo/lists"}