From 6d571b37c559b8534f988e40f76d1a21a974b0b2 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Wed, 14 May 2025 14:37:27 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Rate=20limit=20OTP=20at?= =?UTF-8?q?tempts=20(#1713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/public/locales/en/app.json | 4 ++- .../login/verify/components/otp-form.tsx | 36 ++++++++++++++----- .../src/app/api/auth/[...nextauth]/route.ts | 17 ++++++++- apps/web/src/auth/providers/email.ts | 1 + 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 2e711fc07..bc8581c5c 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -338,5 +338,7 @@ "helpUsImproveDesc": "Take a few minutes to share your feedback and help us shape the future of Rallly.", "giveFeedback": "Give feedback", "homeActionsTitle": "Actions", - "dismissFeedback": "Don't show again" + "dismissFeedback": "Don't show again", + "tooManyAttempts": "Too many attempts, please try again later.", + "unknownError": "Something went wrong" } diff --git a/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx b/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx index f776a09c1..b29ccfcdb 100644 --- a/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx @@ -41,16 +41,36 @@ export function OTPForm({ email }: { email: string }) { }/api/auth/callback/email?email=${encodeURIComponent(email.toLowerCase())}&token=${data.otp}`; const res = await fetch(url); - const resUrl = new URL(res.url); - const hasError = !!resUrl.searchParams.get("error"); - - if (hasError) { - form.setError("otp", { - message: t("wrongVerificationCode"), - }); + if (!res.ok) { + switch (res.status) { + case 429: + form.setError("otp", { + message: t("tooManyAttempts", { + defaultValue: "Too many attempts, please try again later.", + }), + }); + break; + default: + form.setError("otp", { + message: t("unknownError", { + defaultValue: "Something went wrong", + }), + }); + break; + } } else { - window.location.href = searchParams?.get("redirectTo") ?? "/"; + const resUrl = new URL(res.url); + const hasError = !!resUrl.searchParams.get("error"); + if (hasError) { + form.setError("otp", { + message: t("wrongVerificationCode", { + defaultValue: "The code you entered is incorrect", + }), + }); + } else { + window.location.href = searchParams?.get("redirectTo") ?? "/"; + } } }); diff --git a/apps/web/src/app/api/auth/[...nextauth]/route.ts b/apps/web/src/app/api/auth/[...nextauth]/route.ts index 4b6eabc11..ded00e870 100644 --- a/apps/web/src/app/api/auth/[...nextauth]/route.ts +++ b/apps/web/src/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,20 @@ +import { rateLimit } from "@/features/rate-limit"; import { handlers } from "@/next-auth"; import { withPosthog } from "@/utils/posthog"; +import type { NextRequest } from "next/server"; + +export const GET = withPosthog(async (req: NextRequest) => { + if (req.nextUrl.pathname.includes("callback/email")) { + const { success } = await rateLimit("login_otp_attempt", 20, "15m"); + + if (!success) { + return new Response("Too many requests", { + status: 429, + }); + } + } + + return handlers.GET(req); +}); -export const GET = withPosthog(handlers.GET); export const POST = withPosthog(handlers.POST); diff --git a/apps/web/src/auth/providers/email.ts b/apps/web/src/auth/providers/email.ts index 59db1f48d..a5df9e392 100644 --- a/apps/web/src/auth/providers/email.ts +++ b/apps/web/src/auth/providers/email.ts @@ -9,6 +9,7 @@ export const EmailProvider = NodemailerProvider({ server: "none", // This value is required even though we don't need it from: process.env.NOREPLY_EMAIL, id: "email", + maxAge: 15 * 60, generateVerificationToken() { return generateOtp(); },