diff --git a/login/apps/login/src/app/(login)/saml-post/route.ts b/login/apps/login/src/app/(login)/saml-post/route.ts index f2834f3884..a2061a18e2 100644 --- a/login/apps/login/src/app/(login)/saml-post/route.ts +++ b/login/apps/login/src/app/(login)/saml-post/route.ts @@ -1,22 +1,41 @@ +import { getSAMLFormCookie } from "@/lib/saml"; import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const url = searchParams.get("url"); - const relayState = searchParams.get("RelayState"); - const samlResponse = searchParams.get("SAMLResponse"); + const id = searchParams.get("id"); - if (!url || !relayState || !samlResponse) { - return new NextResponse("Missing required parameters", { status: 400 }); + if (!url) { + return new NextResponse("Missing url parameter", { status: 400 }); } + if (!id) { + return new NextResponse("Missing id parameter", { status: 400 }); + } + + const formData = await getSAMLFormCookie(id); + + const formDataParsed = formData ? JSON.parse(formData) : null; + + if (!formDataParsed) { + return new NextResponse("SAML form data not found", { status: 404 }); + } + + // Generate hidden input fields for all key-value pairs in formDataParsed + const hiddenInputs = Object.entries(formDataParsed) + .map( + ([key, value]) => + ``, + ) + .join("\n "); + // Respond with an HTML form that auto-submits via POST const html = `
- - + ${hiddenInputs} diff --git a/login/apps/login/src/app/login/route.ts b/login/apps/login/src/app/login/route.ts index db67efa229..7b57e1a5e9 100644 --- a/login/apps/login/src/app/login/route.ts +++ b/login/apps/login/src/app/login/route.ts @@ -520,16 +520,24 @@ export async function GET(request: NextRequest) { if (url && binding.case === "redirect") { return NextResponse.redirect(url); } else if (url && binding.case === "post") { - const redirectUrl = constructUrl(request, "/saml-post"); + // Create HTML form that auto-submits via POST and escape the SAML cookie + const html = ` + + + + + + +
+ + + `; - redirectUrl.searchParams.set("url", url); - redirectUrl.searchParams.set("RelayState", binding.value.relayState); - redirectUrl.searchParams.set( - "SAMLResponse", - binding.value.samlResponse, - ); - - return NextResponse.redirect(redirectUrl.toString()); + return new NextResponse(html, { + headers: { "Content-Type": "text/html" }, + }); } else { console.log( "could not create response, redirect user to choose other account", diff --git a/login/apps/login/src/lib/saml.ts b/login/apps/login/src/lib/saml.ts index e85084f022..e1b5f4c080 100644 --- a/login/apps/login/src/lib/saml.ts +++ b/login/apps/login/src/lib/saml.ts @@ -4,7 +4,9 @@ import { createResponse, getLoginSettings } from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { v4 as uuidv4 } from "uuid"; import { constructUrl } from "./service-url"; import { isSessionValid } from "./session"; @@ -17,6 +19,37 @@ type LoginWithSAMLAndSession = { request: NextRequest; }; +export async function getSAMLFormUID() { + return uuidv4(); +} + +export async function setSAMLFormCookie(value: string): Promise { + const cookiesList = await cookies(); + + const uid = await getSAMLFormUID(); + + await cookiesList.set({ + name: uid, + value: value, + httpOnly: true, + path: "/", + maxAge: 5 * 60, // 5 minutes + }); + + return uid; +} + +export async function getSAMLFormCookie(uid: string): Promise { + const cookiesList = await cookies(); + + const cookie = cookiesList.get(uid); + if (!cookie || !cookie.value) { + return null; + } + + return cookie.value; +} + export async function loginWithSAMLAndSession({ serviceUrl, samlRequest, diff --git a/login/apps/login/src/lib/zitadel.ts b/login/apps/login/src/lib/zitadel.ts index 442c2be85c..d8b4e5fb51 100644 --- a/login/apps/login/src/lib/zitadel.ts +++ b/login/apps/login/src/lib/zitadel.ts @@ -52,6 +52,7 @@ import { } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { unstable_cacheLife as cacheLife } from "next/cache"; import { getUserAgent } from "./fingerprint"; +import { setSAMLFormCookie } from "./saml"; import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -981,18 +982,15 @@ export async function startIdentityProviderFlow({ value: urls, }, }) - .then((resp) => { + .then(async (resp) => { if (resp.nextStep.case === "authUrl" && resp.nextStep.value) { return resp.nextStep.value; } else if (resp.nextStep.case === "formData" && resp.nextStep.value) { const formData: FormData = resp.nextStep.value; const redirectUrl = "/saml-post"; - const params = new URLSearchParams({ url: formData.url }); - - Object.entries(formData.fields).forEach(([k, v]) => { - params.append(k, v); - }); + const dataId = await setSAMLFormCookie(JSON.stringify(formData.fields)); + const params = new URLSearchParams({ url: formData.url, id: dataId }); return `${redirectUrl}?${params.toString()}`; } else {