Skip to content

Commit e7df05d

Browse files
authored
Merge pull request dubinc#2320 from dubinc/improve-signup-security
Implement OTP rate limiting on the signup
2 parents 08be112 + f6000bb commit e7df05d

1 file changed

Lines changed: 29 additions & 7 deletions

File tree

apps/web/lib/actions/create-user-account.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import { ratelimit } from "@/lib/upstash";
44
import { prisma } from "@dub/prisma";
5+
import { waitUntil } from "@vercel/functions";
56
import { flattenValidationErrors } from "next-safe-action";
67
import { createId } from "../api/create-id";
7-
import { getIP } from "../api/utils";
88
import { hashPassword } from "../auth/password";
99
import z from "../zod";
1010
import { signUpSchema } from "../zod/schemas/auth";
@@ -15,6 +15,9 @@ const schema = signUpSchema.extend({
1515
code: z.string().min(6, "OTP must be 6 characters long."),
1616
});
1717

18+
const MAX_OTP_ATTEMPTS = 5; // Block after 5 failed attempts
19+
const OTP_LOCKOUT_DURATION = "24 h"; // Block for 24 hours
20+
1821
// Sign up a new user using email and password
1922
export const createUserAccountAction = actionClient
2023
.schema(schema, {
@@ -25,26 +28,45 @@ export const createUserAccountAction = actionClient
2528
.action(async ({ parsedInput }) => {
2629
const { email, password, code } = parsedInput;
2730

28-
const { success } = await ratelimit(2, "1 m").limit(`signup:${getIP()}`);
31+
const signupAttemptKey = `signup:attempts:${email}`;
32+
33+
const { remaining: attemptsRemaining } = await ratelimit(
34+
MAX_OTP_ATTEMPTS,
35+
OTP_LOCKOUT_DURATION,
36+
).getRemaining(signupAttemptKey);
2937

30-
if (!success) {
31-
throw new Error("Too many requests. Please try again later.");
38+
if (attemptsRemaining <= 0) {
39+
throw new Error("Too many failed attempts. You have to try again later.");
3240
}
3341

3442
const verificationToken = await prisma.emailVerificationToken.findUnique({
3543
where: {
3644
identifier: email,
3745
token: code,
38-
expires: {
39-
gte: new Date(),
40-
},
4146
},
4247
});
4348

4449
if (!verificationToken) {
50+
await ratelimit(MAX_OTP_ATTEMPTS, OTP_LOCKOUT_DURATION).limit(
51+
signupAttemptKey,
52+
);
53+
4554
throw new Error("Invalid verification code entered.");
4655
}
4756

57+
if (verificationToken.expires && verificationToken.expires < new Date()) {
58+
waitUntil(
59+
prisma.emailVerificationToken.delete({
60+
where: {
61+
identifier: email,
62+
token: code,
63+
},
64+
}),
65+
);
66+
67+
throw new Error("The OTP has expired. Please request a new one.");
68+
}
69+
4870
await prisma.emailVerificationToken.delete({
4971
where: {
5072
identifier: email,

0 commit comments

Comments
 (0)