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.
This commit is contained in:
Max Peintner
2025-07-07 16:11:14 +02:00
committed by GitHub
parent aa8edee50b
commit 253beb4d39
4 changed files with 79 additions and 21 deletions

View File

@@ -1,22 +1,41 @@
import { getSAMLFormCookie } from "@/lib/saml";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const url = searchParams.get("url"); const url = searchParams.get("url");
const relayState = searchParams.get("RelayState"); const id = searchParams.get("id");
const samlResponse = searchParams.get("SAMLResponse");
if (!url || !relayState || !samlResponse) { if (!url) {
return new NextResponse("Missing required parameters", { status: 400 }); 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]) =>
`<input type="hidden" name="${key}" value="${value}" />`,
)
.join("\n ");
// Respond with an HTML form that auto-submits via POST // Respond with an HTML form that auto-submits via POST
const html = ` const html = `
<html> <html>
<body onload="document.forms[0].submit()"> <body onload="document.forms[0].submit()">
<form action="${url}" method="post"> <form action="${url}" method="post">
<input type="hidden" name="RelayState" value="${relayState}" /> ${hiddenInputs}
<input type="hidden" name="SAMLResponse" value="${samlResponse}" />
<noscript> <noscript>
<button type="submit">Continue</button> <button type="submit">Continue</button>
</noscript> </noscript>

View File

@@ -520,16 +520,24 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") { if (url && binding.case === "redirect") {
return NextResponse.redirect(url); return NextResponse.redirect(url);
} else if (url && binding.case === "post") { } 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 = `
<html>
<body onload="document.forms[0].submit()">
<form action="${url}" method="post">
<input type="hidden" name="RelayState" value="${binding.value.relayState}" />
<input type="hidden" name="SAMLResponse" value="${binding.value.samlResponse}" />
<noscript>
<button type="submit">Continue</button>
</noscript>
</form>
</body>
</html>
`;
redirectUrl.searchParams.set("url", url); return new NextResponse(html, {
redirectUrl.searchParams.set("RelayState", binding.value.relayState); headers: { "Content-Type": "text/html" },
redirectUrl.searchParams.set( });
"SAMLResponse",
binding.value.samlResponse,
);
return NextResponse.redirect(redirectUrl.toString());
} else { } else {
console.log( console.log(
"could not create response, redirect user to choose other account", "could not create response, redirect user to choose other account",

View File

@@ -4,7 +4,9 @@ import { createResponse, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { constructUrl } from "./service-url"; import { constructUrl } from "./service-url";
import { isSessionValid } from "./session"; import { isSessionValid } from "./session";
@@ -17,6 +19,37 @@ type LoginWithSAMLAndSession = {
request: NextRequest; request: NextRequest;
}; };
export async function getSAMLFormUID() {
return uuidv4();
}
export async function setSAMLFormCookie(value: string): Promise<string> {
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<string | null> {
const cookiesList = await cookies();
const cookie = cookiesList.get(uid);
if (!cookie || !cookie.value) {
return null;
}
return cookie.value;
}
export async function loginWithSAMLAndSession({ export async function loginWithSAMLAndSession({
serviceUrl, serviceUrl,
samlRequest, samlRequest,

View File

@@ -52,6 +52,7 @@ import {
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { unstable_cacheLife as cacheLife } from "next/cache"; import { unstable_cacheLife as cacheLife } from "next/cache";
import { getUserAgent } from "./fingerprint"; import { getUserAgent } from "./fingerprint";
import { setSAMLFormCookie } from "./saml";
import { createServiceForHost } from "./service"; import { createServiceForHost } from "./service";
const useCache = process.env.DEBUG !== "true"; const useCache = process.env.DEBUG !== "true";
@@ -981,18 +982,15 @@ export async function startIdentityProviderFlow({
value: urls, value: urls,
}, },
}) })
.then((resp) => { .then(async (resp) => {
if (resp.nextStep.case === "authUrl" && resp.nextStep.value) { if (resp.nextStep.case === "authUrl" && resp.nextStep.value) {
return resp.nextStep.value; return resp.nextStep.value;
} else if (resp.nextStep.case === "formData" && resp.nextStep.value) { } else if (resp.nextStep.case === "formData" && resp.nextStep.value) {
const formData: FormData = resp.nextStep.value; const formData: FormData = resp.nextStep.value;
const redirectUrl = "/saml-post"; const redirectUrl = "/saml-post";
const params = new URLSearchParams({ url: formData.url }); const dataId = await setSAMLFormCookie(JSON.stringify(formData.fields));
const params = new URLSearchParams({ url: formData.url, id: dataId });
Object.entries(formData.fields).forEach(([k, v]) => {
params.append(k, v);
});
return `${redirectUrl}?${params.toString()}`; return `${redirectUrl}?${params.toString()}`;
} else { } else {