Files
calendar/apps/web/app/api/auth/reset-password/route.ts
T
sean-brydonandGitHub 13103c0881 feat: Improve error handling in ratelimit (#25223)
* Improve error handling in ratelimit

* Update route.ts

* Update route.ts

* Update route.ts

* address feedback
2025-12-12 20:39:23 +00:00

105 lines
3.3 KiB
TypeScript

import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { parseRequestData } from "app/api/parseRequestData";
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { validPassword } from "@calcom/features/auth/lib/validPassword";
import { hashPassword } from "@calcom/lib/auth/hashPassword";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import getIP from "@calcom/lib/getIP";
import { piiHasher } from "@calcom/lib/server/PiiHasher";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
const passwordResetRequestSchema = z.object({
csrfToken: z.string(),
password: z.string().refine(validPassword, () => ({
message: "Password does not meet the requirements",
})),
requestId: z.string(), // format doesn't matter.
});
async function handler(req: NextRequest) {
const body = await parseRequestData(req);
const {
password: rawPassword,
requestId: rawRequestId,
csrfToken: submittedToken,
} = passwordResetRequestSchema.parse(body);
const cookieStore = await cookies();
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
if (submittedToken !== cookieToken) {
return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
}
// token verified, delete the cookie / a resubmit on failure requires a new csrf token.
cookieStore.delete("calcom.csrf_token");
const remoteIp = getIP(req);
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `api:reset-password:${piiHasher.hash(remoteIp)}`,
});
// Note: There is a low, very low chance that a password request stays valid long enough
// to brute force 3.8126967e+40 options, but rate limiting provides additional protection.
const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
where: {
id: rawRequestId,
expires: {
gt: new Date(),
},
},
select: {
email: true,
},
});
const hashedPassword = await hashPassword(rawPassword);
// this can fail if a password request has been made for an email that has since changed or-
// never existed within Cal. In this case we do not want to disclose the email's existence.
// instead, we just return 404
try {
await prisma.user.update({
where: {
email: maybeRequest.email,
},
data: {
password: {
upsert: {
create: { hash: hashedPassword },
update: { hash: hashedPassword },
},
},
emailVerified: new Date(),
identityProvider: IdentityProvider.CAL,
identityProviderId: null,
},
});
} catch (e) {
return NextResponse.json({}, { status: 404 });
}
await expireResetPasswordRequest(rawRequestId);
return NextResponse.json({ message: "Password reset." }, { status: 201 });
}
async function expireResetPasswordRequest(rawRequestId: string) {
await prisma.resetPasswordRequest.update({
where: {
id: rawRequestId,
},
data: {
// We set the expiry to now to invalidate the request
expires: new Date(),
},
});
}
export const POST = defaultResponderForAppDir(handler);