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 f2834f388..a2061a18e 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 db67efa22..7b57e1a5e 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 e85084f02..e1b5f4c08 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 442c2be85..d8b4e5fb5 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 {