From 517d50932117bda66a44010149e721feb4502051 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Tue, 11 Mar 2025 03:51:33 -0400 Subject: [PATCH] fix: `req.formData()` should be used in some API routes (#19930) --- .../web/app/api/auth/forgot-password/route.ts | 4 +++- .../app/api/auth/oauth/refreshToken/route.ts | 7 ++++++- apps/web/app/api/auth/oauth/token/route.ts | 3 ++- apps/web/app/api/auth/reset-password/route.ts | 7 ++++--- apps/web/app/api/auth/saml/authorize/route.ts | 2 +- apps/web/app/api/auth/saml/callback/route.ts | 6 +++++- apps/web/app/api/auth/saml/token/route.ts | 4 +++- apps/web/app/api/auth/setup/route.ts | 4 +++- .../api/auth/two-factor/totp/disable/route.ts | 3 ++- .../api/auth/two-factor/totp/enable/route.ts | 3 ++- .../api/auth/two-factor/totp/setup/route.ts | 3 ++- apps/web/app/api/parseRequestData.ts | 19 +++++++++++++++++++ .../web/app/api/verify-booking-token/route.ts | 3 ++- packages/features/ee/sso/lib/jackson.ts | 5 +++++ 14 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 apps/web/app/api/parseRequestData.ts diff --git a/apps/web/app/api/auth/forgot-password/route.ts b/apps/web/app/api/auth/forgot-password/route.ts index 084e30ef8c..249b6125a6 100644 --- a/apps/web/app/api/auth/forgot-password/route.ts +++ b/apps/web/app/api/auth/forgot-password/route.ts @@ -1,4 +1,5 @@ import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; @@ -8,7 +9,8 @@ import { emailSchema } from "@calcom/lib/emailSchema"; import prisma from "@calcom/prisma"; async function handler(req: NextRequest) { - const email = emailSchema.transform((val) => val.toLowerCase()).safeParse((await req.json())?.email); + const body = await parseRequestData(req); + const email = emailSchema.transform((val) => val.toLowerCase()).safeParse(body?.email); if (!email.success) { return NextResponse.json({ message: "email is required" }, { status: 400 }); diff --git a/apps/web/app/api/auth/oauth/refreshToken/route.ts b/apps/web/app/api/auth/oauth/refreshToken/route.ts index 591ce4e541..09e9aa03c6 100644 --- a/apps/web/app/api/auth/oauth/refreshToken/route.ts +++ b/apps/web/app/api/auth/oauth/refreshToken/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import jwt from "jsonwebtoken"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -7,7 +8,11 @@ import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClie import type { OAuthTokenPayload } from "@calcom/types/oauth"; export async function POST(req: NextRequest) { - const { client_id, client_secret, grant_type } = await req.json(); + const { client_id, client_secret, grant_type } = await parseRequestData(req); + + if (!client_id || !client_secret) { + return NextResponse.json({ message: "Missing client id or secret" }, { status: 400 }); + } if (grant_type !== "refresh_token") { return NextResponse.json({ message: "grant type invalid" }, { status: 400 }); diff --git a/apps/web/app/api/auth/oauth/token/route.ts b/apps/web/app/api/auth/oauth/token/route.ts index bc956fc87f..3b8862810a 100644 --- a/apps/web/app/api/auth/oauth/token/route.ts +++ b/apps/web/app/api/auth/oauth/token/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import jwt from "jsonwebtoken"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -7,7 +8,7 @@ import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClie import type { OAuthTokenPayload } from "@calcom/types/oauth"; export async function POST(req: NextRequest) { - const { code, client_id, client_secret, grant_type, redirect_uri } = await req.json(); + const { code, client_id, client_secret, grant_type, redirect_uri } = await parseRequestData(req); if (grant_type !== "authorization_code") { return NextResponse.json({ message: "grant_type invalid" }, { status: 400 }); diff --git a/apps/web/app/api/auth/reset-password/route.ts b/apps/web/app/api/auth/reset-password/route.ts index 95eaa80b4b..f205a86ae6 100644 --- a/apps/web/app/api/auth/reset-password/route.ts +++ b/apps/web/app/api/auth/reset-password/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; @@ -15,9 +16,9 @@ const passwordResetRequestSchema = z.object({ }); async function handler(req: NextRequest) { - const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse( - await req.json() - ); + const body = await parseRequestData(req); + + const { password: rawPassword, requestId: rawRequestId } = passwordResetRequestSchema.parse(body); // rate-limited there is a low, very low chance that a password request stays valid long enough // to brute force 3.8126967e+40 options. const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({ diff --git a/apps/web/app/api/auth/saml/authorize/route.ts b/apps/web/app/api/auth/saml/authorize/route.ts index 91e1ba564b..2a35d9bc1b 100644 --- a/apps/web/app/api/auth/saml/authorize/route.ts +++ b/apps/web/app/api/auth/saml/authorize/route.ts @@ -1,8 +1,8 @@ -import type { OAuthReq } from "@boxyhq/saml-jackson"; import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import type { OAuthReq } from "@calcom/features/ee/sso/lib/jackson"; import jackson from "@calcom/features/ee/sso/lib/jackson"; import type { HttpError } from "@calcom/lib/http-error"; diff --git a/apps/web/app/api/auth/saml/callback/route.ts b/apps/web/app/api/auth/saml/callback/route.ts index 6a1aac72b9..59c0208b1a 100644 --- a/apps/web/app/api/auth/saml/callback/route.ts +++ b/apps/web/app/api/auth/saml/callback/route.ts @@ -1,13 +1,17 @@ import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import jackson from "@calcom/features/ee/sso/lib/jackson"; +import type { SAMLResponsePayload } from "@calcom/features/ee/sso/lib/jackson"; async function handler(req: NextRequest) { const { oauthController } = await jackson(); - const { redirect_url } = await oauthController.samlResponse(await req.json()); + const { redirect_url } = await oauthController.samlResponse( + (await parseRequestData(req)) as SAMLResponsePayload + ); if (redirect_url) { return NextResponse.redirect(redirect_url, 302); diff --git a/apps/web/app/api/auth/saml/token/route.ts b/apps/web/app/api/auth/saml/token/route.ts index 34927b02af..56a4f4fa84 100644 --- a/apps/web/app/api/auth/saml/token/route.ts +++ b/apps/web/app/api/auth/saml/token/route.ts @@ -1,12 +1,14 @@ import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import jackson from "@calcom/features/ee/sso/lib/jackson"; +import type { OAuthTokenReq } from "@calcom/features/ee/sso/lib/jackson"; async function handler(req: NextRequest) { const { oauthController } = await jackson(); - const tokenResponse = await oauthController.token(await req.json()); + const tokenResponse = await oauthController.token((await parseRequestData(req)) as OAuthTokenReq); return NextResponse.json(tokenResponse); } diff --git a/apps/web/app/api/auth/setup/route.ts b/apps/web/app/api/auth/setup/route.ts index 23ef90eb47..3db3e5ae7e 100644 --- a/apps/web/app/api/auth/setup/route.ts +++ b/apps/web/app/api/auth/setup/route.ts @@ -1,4 +1,5 @@ import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import { parseRequestData } from "app/api/parseRequestData"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import z from "zod"; @@ -29,8 +30,9 @@ async function handler(req: NextRequest) { if (userCount !== 0) { throw new HttpError({ statusCode: 400, message: "No setup needed." }); } + const body = await parseRequestData(req); - const parsedQuery = querySchema.safeParse(await req.json()); + const parsedQuery = querySchema.safeParse(body); if (!parsedQuery.success) { throw new HttpError({ statusCode: 422, message: parsedQuery.error.message }); } diff --git a/apps/web/app/api/auth/two-factor/totp/disable/route.ts b/apps/web/app/api/auth/two-factor/totp/disable/route.ts index b7a229fcea..b9bfceeaac 100644 --- a/apps/web/app/api/auth/two-factor/totp/disable/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/disable/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -13,7 +14,7 @@ import { IdentityProvider } from "@calcom/prisma/client"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export async function POST(req: NextRequest) { - const body = await req.json(); + const body = await parseRequestData(req); const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); if (!session) { diff --git a/apps/web/app/api/auth/two-factor/totp/enable/route.ts b/apps/web/app/api/auth/two-factor/totp/enable/route.ts index c604b050e9..d1a0e3349c 100644 --- a/apps/web/app/api/auth/two-factor/totp/enable/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/enable/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -11,7 +12,7 @@ import prisma from "@calcom/prisma"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export async function POST(req: NextRequest) { - const body = await req.json(); + const body = await parseRequestData(req); const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); if (!session) { diff --git a/apps/web/app/api/auth/two-factor/totp/setup/route.ts b/apps/web/app/api/auth/two-factor/totp/setup/route.ts index 5fc859dfaa..0d24983d27 100644 --- a/apps/web/app/api/auth/two-factor/totp/setup/route.ts +++ b/apps/web/app/api/auth/two-factor/totp/setup/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import crypto from "crypto"; import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; @@ -15,7 +16,7 @@ import { IdentityProvider } from "@calcom/prisma/enums"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export async function POST(req: NextRequest) { - const body = await req.json(); + const body = await parseRequestData(req); const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) }); if (!session) { diff --git a/apps/web/app/api/parseRequestData.ts b/apps/web/app/api/parseRequestData.ts new file mode 100644 index 0000000000..bfddc3673b --- /dev/null +++ b/apps/web/app/api/parseRequestData.ts @@ -0,0 +1,19 @@ +import type { NextRequest } from "next/server"; + +export async function parseRequestData(req: NextRequest): Promise> { + try { + return await req.json(); + } catch (error) { + const contentType = req.headers.get("content-type") || ""; + // Handle form data requests + if ( + contentType.includes("application/x-www-form-urlencoded") || + contentType.includes("multipart/form-data") + ) { + const formData = await req.formData(); + return Object.fromEntries(formData.entries()); + } + + throw new Error(`Unsupported content type: ${contentType}`); + } +} diff --git a/apps/web/app/api/verify-booking-token/route.ts b/apps/web/app/api/verify-booking-token/route.ts index d0c0a2e220..e35834ef12 100644 --- a/apps/web/app/api/verify-booking-token/route.ts +++ b/apps/web/app/api/verify-booking-token/route.ts @@ -1,3 +1,4 @@ +import { parseRequestData } from "app/api/parseRequestData"; import { headers, cookies } from "next/headers"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; @@ -55,7 +56,7 @@ export async function POST(request: NextRequest) { try { const { action, token, bookingUid, userId } = querySchema.parse(queryParams); - const body = await request.json().catch(() => ({})); + const body = await parseRequestData(request).catch(() => ({})); const { reason } = z.object({ reason: z.string().optional() }).parse(body || {}); return await handleBookingAction(action, token, bookingUid, userId, reason, request); diff --git a/packages/features/ee/sso/lib/jackson.ts b/packages/features/ee/sso/lib/jackson.ts index 96c2197486..c58cab22c4 100644 --- a/packages/features/ee/sso/lib/jackson.ts +++ b/packages/features/ee/sso/lib/jackson.ts @@ -4,12 +4,17 @@ import type { ISPSSOConfig, JacksonOption, IDirectorySyncController, + OAuthTokenReq, + OAuthReq, + SAMLResponsePayload, } from "@boxyhq/saml-jackson"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { clientSecretVerifier, oidcPath, samlAudience, samlDatabaseUrl, samlPath } from "./saml"; +export type { OAuthTokenReq, OAuthReq, SAMLResponsePayload }; + // Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list const opts: JacksonOption = { externalUrl: WEBAPP_URL,