mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-12 17:49:49 -06:00
feat: rate limiting of auth endpoints (#1227)
This commit is contained in:
committed by
GitHub
parent
3d5fdb39c8
commit
8074d324d4
@@ -57,7 +57,7 @@ const DummyUI: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="w-fit ml-4 p-3">
|
||||
<div className="ml-4 w-fit p-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="xs:text-base w-fit text-xs dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
|
||||
|
||||
@@ -42,29 +42,39 @@ export const SigninForm = ({
|
||||
const onSubmit: SubmitHandler<TSigninFormState> = async (data) => {
|
||||
setLoggingIn(true);
|
||||
|
||||
const signInResponse = await signIn("credentials", {
|
||||
callbackUrl: searchParams?.get("callbackUrl") || "/",
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
...(totpLogin && { totpCode: data.totpCode }),
|
||||
...(totpBackup && { backupCode: data.backupCode }),
|
||||
redirect: false,
|
||||
});
|
||||
try {
|
||||
const signInResponse = await signIn("credentials", {
|
||||
callbackUrl: searchParams?.get("callbackUrl") || "/",
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
...(totpLogin && { totpCode: data.totpCode }),
|
||||
...(totpBackup && { backupCode: data.backupCode }),
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (signInResponse?.error === "second factor required") {
|
||||
setTotpLogin(true);
|
||||
if (signInResponse?.error === "second factor required") {
|
||||
setTotpLogin(true);
|
||||
setLoggingIn(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signInResponse?.error) {
|
||||
setLoggingIn(false);
|
||||
setSignInError(signInResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signInResponse?.error) {
|
||||
router.push(searchParams?.get("callbackUrl") || "/");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.toString();
|
||||
const errorFeedback = errorMessage.includes("Invalid URL")
|
||||
? "Too many requests, please try again after some time!"
|
||||
: error.message;
|
||||
setSignInError(errorFeedback);
|
||||
} finally {
|
||||
setLoggingIn(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signInResponse?.error) {
|
||||
setLoggingIn(false);
|
||||
setSignInError(signInResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signInResponse?.error) {
|
||||
router.push(searchParams?.get("callbackUrl") || "/");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
27
apps/web/app/(auth)/auth/rate-limit.ts
Normal file
27
apps/web/app/(auth)/auth/rate-limit.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
type Options = {
|
||||
interval: number;
|
||||
};
|
||||
|
||||
export default function rateLimit(options: Options) {
|
||||
const tokenCache = new LRUCache({
|
||||
max: 1000, // Max 1000 unique IP sessions per 15 minutes
|
||||
ttl: options.interval,
|
||||
});
|
||||
|
||||
return {
|
||||
check: (token: string) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const tokenCount = (tokenCache.get(token) as number[]) || [0];
|
||||
if (tokenCount[0] === 0) {
|
||||
tokenCache.set(token, tokenCount);
|
||||
}
|
||||
tokenCount[0] += 1;
|
||||
|
||||
const currentUsage = tokenCount[0];
|
||||
const isRateLimited = currentUsage >= 5;
|
||||
return isRateLimited ? reject() : resolve();
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -87,14 +87,14 @@ export const SignupForm = ({
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div className="absolute top-10 rounded-md bg-teal-50 p-4">
|
||||
<div className="absolute top-10 rounded-md bg-red-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<XCircleIcon className="h-5 w-5 text-teal-400" aria-hidden="true" />
|
||||
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-teal-800">An error occurred when logging you in</h3>
|
||||
<div className="mt-2 text-sm text-teal-700">
|
||||
<h3 className="text-sm font-medium text-red-800">An error occurred when signing you up</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p className="space-y-1 whitespace-pre-wrap">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
apps/web/middleware.ts
Normal file
47
apps/web/middleware.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import rateLimit from "@/app/(auth)/auth/rate-limit";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
const signUpLimiter = rateLimit({ interval: 60 * 60 * 1000 }); // 60 minutes
|
||||
const loginLimiter = rateLimit({ interval: 15 * 60 * 1000 }); // 15 minutes
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
if (process.env.IS_FORMBRICKS_CLOUD != "1") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
const res = NextResponse.next();
|
||||
let ip = request.ip ?? request.headers.get("x-real-ip");
|
||||
const forwardedFor = request.headers.get("x-forwarded-for");
|
||||
if (!ip && forwardedFor) {
|
||||
ip = forwardedFor.split(",").at(0) ?? null;
|
||||
}
|
||||
|
||||
if (ip) {
|
||||
try {
|
||||
if (request.nextUrl.pathname === "/api/auth/callback/credentials") {
|
||||
await loginLimiter.check(ip);
|
||||
} else if (request.nextUrl.pathname === "/api/v1/users") {
|
||||
await signUpLimiter.check(ip);
|
||||
}
|
||||
return res;
|
||||
} catch (_e) {
|
||||
console.log("Rate Limiting IP: ", ip);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests, Please try after a while!" },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests, Please try after a while!" },
|
||||
{ status: 429 }
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/auth/callback/credentials", "/api/v1/users"],
|
||||
};
|
||||
@@ -34,6 +34,7 @@
|
||||
"googleapis": "^127.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^10.0.1",
|
||||
"lucide-react": "^0.287.0",
|
||||
"mime": "^3.0.0",
|
||||
"next": "13.5.5",
|
||||
|
||||
@@ -37,7 +37,7 @@ const SelectContent: React.ComponentType<SelectPrimitive.SelectContentProps> = R
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"animate-in fade-in-80 relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-slate-50 dark:bg-slate-700 text-slate-700 shadow-md dark:text-slate-300",
|
||||
"animate-in fade-in-80 relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-slate-50 text-slate-700 shadow-md dark:bg-slate-700 dark:text-slate-300",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
|
||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@@ -28,7 +28,7 @@ importers:
|
||||
version: 3.13.0
|
||||
turbo:
|
||||
specifier: latest
|
||||
version: 1.10.13
|
||||
version: 1.10.14
|
||||
|
||||
apps/demo:
|
||||
dependencies:
|
||||
@@ -350,6 +350,9 @@ importers:
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
lru-cache:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
lucide-react:
|
||||
specifier: ^0.287.0
|
||||
version: 0.287.0(react@18.2.0)
|
||||
@@ -517,7 +520,7 @@ importers:
|
||||
version: 9.0.0(eslint@8.51.0)
|
||||
eslint-config-turbo:
|
||||
specifier: latest
|
||||
version: 1.8.8(eslint@8.51.0)
|
||||
version: 1.10.14(eslint@8.51.0)
|
||||
eslint-plugin-react:
|
||||
specifier: 7.33.2
|
||||
version: 7.33.2(eslint@8.51.0)
|
||||
@@ -12398,13 +12401,13 @@ packages:
|
||||
resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==}
|
||||
dev: true
|
||||
|
||||
/eslint-config-turbo@1.8.8(eslint@8.51.0):
|
||||
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
|
||||
/eslint-config-turbo@1.10.14(eslint@8.51.0):
|
||||
resolution: {integrity: sha512-ZeB+IcuFXy1OICkLuAplVa0euoYbhK+bMEQd0nH9+Lns18lgZRm33mVz/iSoH9VdUzl/1ZmFmoK+RpZc+8R80A==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
eslint: 8.51.0
|
||||
eslint-plugin-turbo: 1.8.8(eslint@8.51.0)
|
||||
eslint-plugin-turbo: 1.10.14(eslint@8.51.0)
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-node@0.3.9:
|
||||
@@ -12603,11 +12606,12 @@ packages:
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-turbo@1.8.8(eslint@8.51.0):
|
||||
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
|
||||
/eslint-plugin-turbo@1.10.14(eslint@8.51.0):
|
||||
resolution: {integrity: sha512-sBdBDnYr9AjT1g4lR3PBkZDonTrMnR4TvuGv5W0OiF7z9az1rI68yj2UHJZvjkwwcGu5mazWA1AfB0oaagpmfg==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 8.51.0
|
||||
dev: true
|
||||
|
||||
@@ -16413,6 +16417,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/lru-cache@10.0.1:
|
||||
resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
|
||||
/lru-cache@4.1.5:
|
||||
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
|
||||
dependencies:
|
||||
@@ -16430,10 +16438,6 @@ packages:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
/lru-cache@9.1.1:
|
||||
resolution: {integrity: sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
|
||||
/lru_map@0.3.3:
|
||||
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
|
||||
dev: false
|
||||
@@ -18413,7 +18417,7 @@ packages:
|
||||
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
lru-cache: 9.1.1
|
||||
lru-cache: 10.0.1
|
||||
minipass: 5.0.0
|
||||
|
||||
/path-to-regexp@0.1.7:
|
||||
@@ -21725,64 +21729,64 @@ packages:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
/turbo-darwin-64@1.10.13:
|
||||
resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==}
|
||||
/turbo-darwin-64@1.10.14:
|
||||
resolution: {integrity: sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-darwin-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==}
|
||||
/turbo-darwin-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-64@1.10.13:
|
||||
resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==}
|
||||
/turbo-linux-64@1.10.14:
|
||||
resolution: {integrity: sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==}
|
||||
/turbo-linux-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-64@1.10.13:
|
||||
resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==}
|
||||
/turbo-windows-64@1.10.14:
|
||||
resolution: {integrity: sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==}
|
||||
/turbo-windows-arm64@1.10.14:
|
||||
resolution: {integrity: sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo@1.10.13:
|
||||
resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==}
|
||||
/turbo@1.10.14:
|
||||
resolution: {integrity: sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 1.10.13
|
||||
turbo-darwin-arm64: 1.10.13
|
||||
turbo-linux-64: 1.10.13
|
||||
turbo-linux-arm64: 1.10.13
|
||||
turbo-windows-64: 1.10.13
|
||||
turbo-windows-arm64: 1.10.13
|
||||
turbo-darwin-64: 1.10.14
|
||||
turbo-darwin-arm64: 1.10.14
|
||||
turbo-linux-64: 1.10.14
|
||||
turbo-linux-arm64: 1.10.14
|
||||
turbo-windows-64: 1.10.14
|
||||
turbo-windows-arm64: 1.10.14
|
||||
dev: true
|
||||
|
||||
/tw-to-css@0.0.11:
|
||||
|
||||
Reference in New Issue
Block a user