mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-27 09:42:16 +00:00
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]>
147 lines
4.4 KiB
TypeScript
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;
|
|
}
|