From 253beb4d39fb2a5c81d43654819697d3d71b9e28 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 7 Jul 2025 16:11:14 +0200 Subject: [PATCH] fix(login): encode formpost data to cookie (#10173) This PR implements a SAML cookie which is used to save information to complete the form post. It is primarily used to avoid sending the information as url search params and therefore reducing its length. --- .../login/src/app/(login)/saml-post/route.ts | 31 +++++++++++++---- login/apps/login/src/app/login/route.ts | 26 ++++++++++----- login/apps/login/src/lib/saml.ts | 33 +++++++++++++++++++ login/apps/login/src/lib/zitadel.ts | 10 +++--- 4 files changed, 79 insertions(+), 21 deletions(-) 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 {