Files
zitadel/apps/login/src/lib/client.ts
T
9492c12c09 fix(login): refactor SAML Post Flow to bypass cookie limits, fix SAML SP post (#11170)
Closes #10816
Closes #10759

# Which Problems Are Solved

This PR refactors the SAML Post binding flow to address issues with
payloads exceeding browser cookie size limits (typically ~4KB).
Previously, the SAML form data was serialized into a cookie, and the
user was redirected to an intermediate /saml-post page to render the
form. This approach failed for large SAML assertions.

# How the Problems Are Solved

The new implementation bypasses cookies entirely for the payload
transfer. Server actions now return the form data directly to the
client, which immediately renders and submits the form.

- Direct Data Transfer: Updated `startIdentityProviderFlow` to return an
object { url, fields } instead of a redirect URL when the next step is
formData.
- Server Actions: Updated redirectToIdp, sendLoginname, and
completeAuthFlow to propagate this samlData object to the client.
- Client-Side Auto-Submission: Added a new AutoSubmitForm component that
renders a hidden form with the provided fields and automatically submits
it to the IdP on mount.

---------

Co-authored-by: conblem <[email protected]>
2026-02-09 13:46:30 +01:00

147 lines
4.4 KiB
TypeScript

import { completeAuthFlow } from "./server/auth-flow";
type FinishFlowCommand =
| {
sessionId: string;
requestId: string;
}
| { loginName: string };
function goToSignedInPage(
props:
| { sessionId: string; organization?: string; requestId?: string }
| { organization?: string; loginName: string; requestId?: string },
) {
const params = new URLSearchParams({});
if ("loginName" in props && props.loginName) {
params.append("loginName", props.loginName);
}
if ("sessionId" in props && props.sessionId) {
params.append("sessionId", props.sessionId);
}
if (props.organization) {
params.append("organization", props.organization);
}
// required to show conditional UI for device flow
if (props.requestId) {
params.append("requestId", props.requestId);
}
return `/signedin?` + params;
}
/**
* Complete authentication flow or get next URL for navigation
* - For OIDC/SAML flows with sessionId+requestId: completes flow directly via server action
* - For device flows: returns URL to signed-in page
* - For other cases: returns default redirect or fallback URL
*/
export async function completeFlowOrGetUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<{ redirect: string } | { error: string } | { samlData: { url: string; fields: Record<string, string> } }> {
console.log("completeFlowOrGetUrl called with:", command, "defaultRedirectUri:", defaultRedirectUri);
// Complete OIDC/SAML flows directly with server action
if (
"sessionId" in command &&
"requestId" in command &&
(command.requestId.startsWith("saml_") || command.requestId.startsWith("oidc_"))
) {
console.log("completeFlowOrGetUrl: OIDC/SAML flow detected");
// This completes the flow and returns a redirect URL or error
const result = await completeAuthFlow({
sessionId: command.sessionId,
requestId: command.requestId,
});
console.log("completeFlowOrGetUrl: got OIDC/SAML flow result");
return result;
}
console.log("completeFlowOrGetUrl: Regular flow, getting next URL");
// For all other cases, return URL for navigation
const url = await getNextUrl(command, defaultRedirectUri);
console.log("completeFlowOrGetUrl: got Next URL:", url);
const result = { redirect: url };
console.log("completeFlowOrGetUrl: got final result");
return result;
}
/**
* for client: redirects user back to device flow completion, default redirect, or success page
* Note: OIDC/SAML flows now use completeAuthFlowAction() instead of URL navigation
* @param command
* @returns
*/
export async function getNextUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<string> {
console.log("getNextUrl called with:", command, "defaultRedirectUri:", defaultRedirectUri);
// finish Device Authorization Flow
if (
"requestId" in command &&
command.requestId.startsWith("device_") &&
("loginName" in command || "sessionId" in command)
) {
const result = goToSignedInPage({
...command,
organization: command.organization,
});
console.log("getNextUrl: Got Device flow result");
return result;
}
// OIDC/SAML flows are now handled by completeAuthFlowAction() server action
// This function only handles device flows and fallback navigation
if (defaultRedirectUri) {
console.log("getNextUrl: Using defaultRedirectUri:", defaultRedirectUri);
return defaultRedirectUri;
}
const result = goToSignedInPage(command);
console.log("getNextUrl: got goToSignedInPage result");
return result;
}
export type ServerActionResponse =
| { redirect: string }
| { error: string }
| { samlData: { url: string; fields: Record<string, string> } }
| undefined
| null;
export function handleServerActionResponse(
response: ServerActionResponse,
router: { push: (url: string) => void },
setSamlData: (data: { url: string; fields: Record<string, string> }) => void,
setError: (error: string) => void,
): boolean {
if (!response) {
return false;
}
if ("redirect" in response && response.redirect) {
router.push(response.redirect);
return true;
}
if ("samlData" in response && response.samlData) {
setSamlData(response.samlData);
return true;
}
if ("error" in response && response.error) {
setError(response.error);
return true;
}
return false;
}