Files
zitadel/apps/login/src/lib/client.ts
T
Max PeintnerandGitHub 637b370c17 chore(login): Extract auth flow utilities and eliminate RSC request interference (#10644)
The /login route was experiencing issues with React Server Component
(RSC) requests interfering with one-time authentication callbacks. When
users navigated to /login via client-side routing (router.push()),
Next.js automatically triggered _rsc requests that could consume
single-use createCallback tokens, breaking OIDC and SAML authentication
flows.

# Which Problems Are Solved

When users attempt to log in, Next.js automatically makes requests with
the `_rsc=1` query parameter for React Server Components. The current
implementation treats these as server errors:

```typescript
// Before
if (_rsc) {
  return NextResponse.json({ error: "No _rsc supported" }, { status: 500 });
}
```

This results in:
- Spurious 500 error logs polluting monitoring systems
- False alerts for server failures 
- Difficulty distinguishing real issues from benign RSC requests

# How the Problems Are Solved

This PR implements a comprehensive refactoring that:

- Eliminates RSC interference by providing server actions for internal
auth flow completion
- Separates concerns between external flow initiation and internal flow
completion
- Extracts shared utilities to improve code maintainability and
reusability
- Maintains full backward compatibility for external applications

# Additional Context

## New Architecture
- auth-flow.ts: Shared utilities for auth flow completion with RSC
protection
- flow-initiation.ts: Extracted OIDC/SAML flow initiation logic (~400
lines)
- auth.ts: Server actions for internal components

## Route Handler Simplification
- route.ts: Reduced from ~350 lines to ~75 lines
- External-only focus: Now handles only flow initiation for external
applications
- Removed completion logic: External apps use their own callback URLs
- Enhanced validation: Early RSC blocking and parameter validation

## Flow Logic Improvements
- Early return patterns: Guard clauses eliminate deep nesting
- Better error handling: Specific error messages for different failure
modes
- Fixed SAML flow: Addressed incomplete logic
- Consistent session handling: Unified approach across OIDC and SAML
2025-09-22 16:09:20 +00:00

112 lines
3.5 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 }> {
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: OIDC/SAML flow result:", 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: Next URL:", url);
const result = { redirect: url };
console.log("completeFlowOrGetUrl: Final result:", 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: Device flow result:", 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: Using goToSignedInPage result:", result);
return result;
}