mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-28 10:02:15 +00:00
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
112 lines
3.5 KiB
TypeScript
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;
|
|
}
|