From 8d18b3da38c1529d8842e2fc2b3a0b68c286f452 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Mon, 10 Mar 2025 10:36:11 -0400 Subject: [PATCH] chore: migrate saml/oidc APIs to App Router (#19515) * migrate oidc * migrate saml apis * remove comment * add force-dynamic * fix * fix * refactor --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --- apps/web/app/api/auth/oidc/route.ts | 41 +++++++++++++++++++ apps/web/app/api/auth/saml/authorize/route.ts | 29 +++++++++++++ apps/web/app/api/auth/saml/callback/route.ts | 21 ++++++++++ apps/web/app/api/auth/saml/token/route.ts | 15 +++++++ apps/web/app/api/auth/saml/userinfo/route.ts | 38 +++++++++++++++++ apps/web/pages/api/auth/oidc.ts | 36 ---------------- apps/web/pages/api/auth/saml/authorize.ts | 23 ----------- apps/web/pages/api/auth/saml/callback.ts | 19 --------- apps/web/pages/api/auth/saml/token.ts | 14 ------- apps/web/pages/api/auth/saml/userinfo.ts | 35 ---------------- 10 files changed, 144 insertions(+), 127 deletions(-) create mode 100644 apps/web/app/api/auth/oidc/route.ts create mode 100644 apps/web/app/api/auth/saml/authorize/route.ts create mode 100644 apps/web/app/api/auth/saml/callback/route.ts create mode 100644 apps/web/app/api/auth/saml/token/route.ts create mode 100644 apps/web/app/api/auth/saml/userinfo/route.ts delete mode 100644 apps/web/pages/api/auth/oidc.ts delete mode 100644 apps/web/pages/api/auth/saml/authorize.ts delete mode 100644 apps/web/pages/api/auth/saml/callback.ts delete mode 100644 apps/web/pages/api/auth/saml/token.ts delete mode 100644 apps/web/pages/api/auth/saml/userinfo.ts diff --git a/apps/web/app/api/auth/oidc/route.ts b/apps/web/app/api/auth/oidc/route.ts new file mode 100644 index 0000000000..ecf4690777 --- /dev/null +++ b/apps/web/app/api/auth/oidc/route.ts @@ -0,0 +1,41 @@ +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { HttpError } from "@calcom/lib/http-error"; + +// This is the callback endpoint for the OIDC provider +// A team must set this endpoint in the OIDC provider's configuration +async function handler(req: NextRequest) { + const { searchParams } = req.nextUrl; + const code = searchParams.get("code"); + const state = searchParams.get("state"); + + if (!code || !state) { + return NextResponse.json({ message: "Code and state are required" }, { status: 400 }); + } + + const { oauthController } = await jackson(); + + try { + const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state }); + + if (!redirect_url) { + throw new HttpError({ + message: "No redirect URL found", + statusCode: 500, + }); + } + + return NextResponse.redirect(redirect_url, 302); + } catch (err) { + const { message, statusCode = 500 } = err as HttpError; + + return NextResponse.json({ message }, { status: statusCode }); + } +} + +const getHandler = defaultResponderForAppDir(handler); + +export { getHandler as GET }; diff --git a/apps/web/app/api/auth/saml/authorize/route.ts b/apps/web/app/api/auth/saml/authorize/route.ts new file mode 100644 index 0000000000..91e1ba564b --- /dev/null +++ b/apps/web/app/api/auth/saml/authorize/route.ts @@ -0,0 +1,29 @@ +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 jackson from "@calcom/features/ee/sso/lib/jackson"; +import type { HttpError } from "@calcom/lib/http-error"; + +async function handler(req: NextRequest) { + const { oauthController } = await jackson(); + + try { + const { redirect_url } = await oauthController.authorize( + Object.fromEntries(req.nextUrl.searchParams) as unknown as OAuthReq + ); + + return NextResponse.redirect(redirect_url as string, 302); + } catch (err) { + const { message, statusCode = 500 } = err as HttpError; + + return NextResponse.json({ message }, { status: statusCode }); + } +} + +const getHandler = defaultResponderForAppDir(handler); + +export { getHandler as GET }; + +export const dynamic = "force-dynamic"; diff --git a/apps/web/app/api/auth/saml/callback/route.ts b/apps/web/app/api/auth/saml/callback/route.ts new file mode 100644 index 0000000000..6a1aac72b9 --- /dev/null +++ b/apps/web/app/api/auth/saml/callback/route.ts @@ -0,0 +1,21 @@ +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import jackson 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()); + + if (redirect_url) { + return NextResponse.redirect(redirect_url, 302); + } + + return NextResponse.json({ message: "No redirect URL provided" }, { status: 400 }); +} + +const postHandler = defaultResponderForAppDir(handler); + +export { postHandler as POST }; diff --git a/apps/web/app/api/auth/saml/token/route.ts b/apps/web/app/api/auth/saml/token/route.ts new file mode 100644 index 0000000000..34927b02af --- /dev/null +++ b/apps/web/app/api/auth/saml/token/route.ts @@ -0,0 +1,15 @@ +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; + +async function handler(req: NextRequest) { + const { oauthController } = await jackson(); + const tokenResponse = await oauthController.token(await req.json()); + return NextResponse.json(tokenResponse); +} + +const postHandler = defaultResponderForAppDir(handler); + +export { postHandler as POST }; diff --git a/apps/web/app/api/auth/saml/userinfo/route.ts b/apps/web/app/api/auth/saml/userinfo/route.ts new file mode 100644 index 0000000000..3a02f2f4fe --- /dev/null +++ b/apps/web/app/api/auth/saml/userinfo/route.ts @@ -0,0 +1,38 @@ +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import z from "zod"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { HttpError } from "@calcom/lib/http-error"; + +const extractAuthToken = (req: NextRequest) => { + const authHeader = req.headers.get("authorization"); + const parts = (authHeader || "").split(" "); + if (parts.length > 1) return parts[1]; + + // check for query param + let arr: string[] = []; + const { access_token } = requestQuery.parse(Object.fromEntries(req.nextUrl.searchParams)); + arr = arr.concat(access_token); + if (arr[0].length > 0) return arr[0]; + + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); +}; + +const requestQuery = z.object({ + access_token: z.string(), +}); + +async function handler(req: NextRequest) { + const { oauthController } = await jackson(); + const token = extractAuthToken(req); + const userInfo = await oauthController.userInfo(token); + return NextResponse.json(userInfo); +} + +const getHandler = defaultResponderForAppDir(handler); + +export { getHandler as GET }; + +export const dynamic = "force-dynamic"; diff --git a/apps/web/pages/api/auth/oidc.ts b/apps/web/pages/api/auth/oidc.ts deleted file mode 100644 index 0e65b2b551..0000000000 --- a/apps/web/pages/api/auth/oidc.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { HttpError } from "@calcom/lib/http-error"; - -// This is the callback endpoint for the OIDC provider -// A team must set this endpoint in the OIDC provider's configuration -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "GET") { - return res.status(400).send("Method not allowed"); - } - - const { code, state } = req.query as { - code: string; - state: string; - }; - - const { oauthController } = await jackson(); - - try { - const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state }); - - if (!redirect_url) { - throw new HttpError({ - message: "No redirect URL found", - statusCode: 500, - }); - } - - return res.redirect(302, redirect_url); - } catch (err) { - const { message, statusCode = 500 } = err as HttpError; - - return res.status(statusCode).send(message); - } -} diff --git a/apps/web/pages/api/auth/saml/authorize.ts b/apps/web/pages/api/auth/saml/authorize.ts deleted file mode 100644 index 337406cb4f..0000000000 --- a/apps/web/pages/api/auth/saml/authorize.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { OAuthReq } from "@boxyhq/saml-jackson"; -import type { NextApiRequest, NextApiResponse } from "next"; - -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import type { HttpError } from "@calcom/lib/http-error"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { oauthController } = await jackson(); - - if (req.method !== "GET") { - return res.status(400).send("Method not allowed"); - } - - try { - const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq); - - return res.redirect(302, redirect_url as string); - } catch (err) { - const { message, statusCode = 500 } = err as HttpError; - - return res.status(statusCode).send(message); - } -} diff --git a/apps/web/pages/api/auth/saml/callback.ts b/apps/web/pages/api/auth/saml/callback.ts deleted file mode 100644 index 9ac6d9f369..0000000000 --- a/apps/web/pages/api/auth/saml/callback.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -async function postHandler(req: NextApiRequest, res: NextApiResponse) { - const { oauthController } = await jackson(); - - const { redirect_url } = await oauthController.samlResponse(req.body); - - if (redirect_url) { - res.redirect(302, redirect_url); - } -} - -export default defaultHandler({ - POST: Promise.resolve({ default: defaultResponder(postHandler) }), -}); diff --git a/apps/web/pages/api/auth/saml/token.ts b/apps/web/pages/api/auth/saml/token.ts deleted file mode 100644 index 683b1fe6d1..0000000000 --- a/apps/web/pages/api/auth/saml/token.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextApiRequest } from "next"; - -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -async function postHandler(req: NextApiRequest) { - const { oauthController } = await jackson(); - return await oauthController.token(req.body); -} - -export default defaultHandler({ - POST: Promise.resolve({ default: defaultResponder(postHandler) }), -}); diff --git a/apps/web/pages/api/auth/saml/userinfo.ts b/apps/web/pages/api/auth/saml/userinfo.ts deleted file mode 100644 index ef55de34ed..0000000000 --- a/apps/web/pages/api/auth/saml/userinfo.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { NextApiRequest } from "next"; -import z from "zod"; - -import jackson from "@calcom/features/ee/sso/lib/jackson"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -const extractAuthToken = (req: NextApiRequest) => { - const authHeader = req.headers["authorization"]; - const parts = (authHeader || "").split(" "); - if (parts.length > 1) return parts[1]; - - // check for query param - let arr: string[] = []; - const { access_token } = requestQuery.parse(req.query); - arr = arr.concat(access_token); - if (arr[0].length > 0) return arr[0]; - - throw new HttpError({ statusCode: 401, message: "Unauthorized" }); -}; - -const requestQuery = z.object({ - access_token: z.string(), -}); - -async function getHandler(req: NextApiRequest) { - const { oauthController } = await jackson(); - const token = extractAuthToken(req); - return await oauthController.userInfo(token); -} - -export default defaultHandler({ - GET: Promise.resolve({ default: defaultResponder(getHandler) }), -});