feat: rate limiting of auth endpoints (#1227)

This commit is contained in:
Shubham Palriwala
2023-10-18 20:21:28 +05:30
committed by GitHub
parent 3d5fdb39c8
commit 8074d324d4
8 changed files with 148 additions and 59 deletions

View File

@@ -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"

View File

@@ -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") || "/");
}
};

View 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();
}),
};
}

View File

@@ -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
View 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"],
};

View File

@@ -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",

View File

@@ -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
View File

@@ -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: