diff --git a/apps/web/app/api/auth/oauth/refreshToken/route.ts b/apps/web/app/api/auth/oauth/refreshToken/route.ts index 09e9aa03c6..0dee336ab7 100644 --- a/apps/web/app/api/auth/oauth/refreshToken/route.ts +++ b/apps/web/app/api/auth/oauth/refreshToken/route.ts @@ -1,4 +1,4 @@ -import { parseRequestData } from "app/api/parseRequestData"; +import { parseUrlFormData } from "app/api/parseRequestData"; import jwt from "jsonwebtoken"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -8,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 { client_id, client_secret, grant_type } = await parseRequestData(req); + const { client_id, client_secret, grant_type } = await parseUrlFormData(req); if (!client_id || !client_secret) { return NextResponse.json({ message: "Missing client id or secret" }, { 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 3b8862810a..8ffb4f9bbe 100644 --- a/apps/web/app/api/auth/oauth/token/route.ts +++ b/apps/web/app/api/auth/oauth/token/route.ts @@ -1,4 +1,4 @@ -import { parseRequestData } from "app/api/parseRequestData"; +import { parseUrlFormData } from "app/api/parseRequestData"; import jwt from "jsonwebtoken"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -8,8 +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 parseRequestData(req); - + const { code, client_id, client_secret, grant_type, redirect_uri } = await parseUrlFormData(req); if (grant_type !== "authorization_code") { return NextResponse.json({ message: "grant_type invalid" }, { status: 400 }); } diff --git a/apps/web/app/api/parseRequestData.ts b/apps/web/app/api/parseRequestData.ts index bfddc3673b..835035091f 100644 --- a/apps/web/app/api/parseRequestData.ts +++ b/apps/web/app/api/parseRequestData.ts @@ -1,19 +1,51 @@ 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()); - } +import { HttpError } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; - throw new Error(`Unsupported content type: ${contentType}`); +const log = logger.getSubLogger({ prefix: ["[parseRequestData]"] }); + +export async function parseUrlFormData(req: NextRequest): Promise> { + try { + // Read raw text body (because Next.js does NOT parse x-www-form-urlencoded automatically) + const rawBody = await req.text(); + const params = new URLSearchParams(rawBody); + return Object.fromEntries(params); + } catch (e) { + log.error(`Invalid Url Form Data: ${e} from path ${req.nextUrl}`); + throw new HttpError({ statusCode: 400, message: "Bad Request (Invalid Url Form Data)" }); } } + +export async function parseMultiFormData(req: NextRequest): Promise> { + try { + const formData = await req.formData(); + return Object.fromEntries(formData.entries()); + } catch (e) { + log.error(`Invalid Multi Form Data: ${e} from path ${req.nextUrl}`); + throw new HttpError({ statusCode: 400, message: "Bad Request (Invalid Multi Form Data)" }); + } +} + +export async function parseRequestData(req: NextRequest): Promise> { + const contentType = req.headers.get("content-type") ?? "application/json"; + if (contentType.includes("application/json")) { + try { + return await req.json(); + } catch (e) { + log.error(`Invalid JSON: ${e} from path ${req.nextUrl}`); + throw new HttpError({ statusCode: 400, message: "Bad Request (Invalid JSON)" }); + } + } + + if (contentType.includes("application/x-www-form-urlencoded")) { + return await parseUrlFormData(req); + } + + if (contentType.includes("multipart/form-data")) { + return await parseMultiFormData(req); + } + + log.error(`Unsupported content type: ${contentType} from path ${req.nextUrl}`); + throw new HttpError({ statusCode: 415, message: `Unsupported Content-Type. Expected ${contentType}` }); +} diff --git a/apps/web/playwright/oauth-provider.e2e.ts b/apps/web/playwright/oauth-provider.e2e.ts index 144c17dc47..b668b15e15 100644 --- a/apps/web/playwright/oauth-provider.e2e.ts +++ b/apps/web/playwright/oauth-provider.e2e.ts @@ -43,17 +43,17 @@ test.describe("OAuth Provider", () => { const code = url.searchParams.get("code"); // request token with authorization code + const tokenForm = new URLSearchParams(); + tokenForm.append("code", code ?? ""); + tokenForm.append("client_id", client.clientId); + tokenForm.append("client_secret", client.orginalSecret); + tokenForm.append("grant_type", "authorization_code"); + tokenForm.append("redirect_uri", client.redirectUri); const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, { - body: JSON.stringify({ - code, - client_id: client.clientId, - client_secret: client.orginalSecret, - grant_type: "authorization_code", - redirect_uri: client.redirectUri, - }), + body: tokenForm.toString(), method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, }); @@ -74,16 +74,16 @@ test.describe("OAuth Provider", () => { expect(meData.username.startsWith("test user")).toBe(true); // request new token with refresh token + const refreshTokenForm = new URLSearchParams(); + refreshTokenForm.append("refresh_token", tokenData.refresh_token); + refreshTokenForm.append("client_id", client.clientId); + refreshTokenForm.append("client_secret", client.orginalSecret); + refreshTokenForm.append("grant_type", "refresh_token"); const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { - body: JSON.stringify({ - refresh_token: tokenData.refresh_token, - client_id: client.clientId, - client_secret: client.orginalSecret, - grant_type: "refresh_token", - }), + body: refreshTokenForm.toString(), method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, }); @@ -126,17 +126,17 @@ test.describe("OAuth Provider", () => { const code = url.searchParams.get("code"); // request token with authorization code + const tokenForm = new URLSearchParams(); + tokenForm.append("code", code ?? ""); + tokenForm.append("client_id", client.clientId); + tokenForm.append("client_secret", client.orginalSecret); + tokenForm.append("grant_type", "authorization_code"); + tokenForm.append("redirect_uri", client.redirectUri); const tokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/token`, { - body: JSON.stringify({ - code, - client_id: client.clientId, - client_secret: client.orginalSecret, - grant_type: "authorization_code", - redirect_uri: client.redirectUri, - }), + body: tokenForm.toString(), method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, }); @@ -157,16 +157,16 @@ test.describe("OAuth Provider", () => { expect(meData.username).toEqual(`user-id-${user.id}'s Team`); // request new token with refresh token + const refreshTokenForm = new URLSearchParams(); + refreshTokenForm.append("refresh_token", tokenData.refresh_token); + refreshTokenForm.append("client_id", client.clientId); + refreshTokenForm.append("client_secret", client.orginalSecret); + refreshTokenForm.append("grant_type", "refresh_token"); const refreshTokenResponse = await fetch(`${WEBAPP_URL}/api/auth/oauth/refreshToken`, { - body: JSON.stringify({ - refresh_token: tokenData.refresh_token, - client_id: client.clientId, - client_secret: client.orginalSecret, - grant_type: "refresh_token", - }), + body: refreshTokenForm.toString(), method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", }, });