Files
zitadel/apps/login/src/components/choose-second-factor-to-setup.tsx
T
d27833e1bc feat(login): redirect configuration options in multi-domain setups (#11527)
# Which Problems Are Solved

This PR introduces robust redirect configuration options and enhances
security for Server Actions in multi-domain setups.

# How the Problems Are Solved

- Redirect Fallback Logic: Implemented a unified `resolveRedirectUri`
utility that prioritizes redirect targets as follows:
  - Environment override (`DEFAULT_REDIRECT_URI`)
  - Organization settings (`defaultRedirectUri`).
  - Relative fallback (`/signedin?...`).
- Host Reflection: `DEFAULT_REDIRECT_URI` supports absolute URLs or
relative paths via host reflection.
- Server Action Security: Configured `serverActions.allowedOrigins` to
be dynamically manageable via the `SERVER_ACTION_ALLOWED_ORIGINS`
environment variable (comma-separated list). It defaults to an empty
list for maximum security.
- UI Integration: Updated the /signedin page to use the unified redirect
logic, ensuring consistent behavior for the "Continue" button.
- Documentation: Added type definitions and detailed descriptions for
new environment variables in
`next-env-vars.d.ts`

---------

Co-authored-by: Copilot <[email protected]>
2026-02-24 11:02:29 +03:00

112 lines
3.4 KiB
TypeScript

"use client";
import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
import { LoginSettings, SecondFactorType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useRouter } from "next/navigation";
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
import { Translated } from "./translated";
import { useState } from "react";
import { handleServerActionResponse } from "@/lib/client-utils";
import { AutoSubmitForm } from "./auto-submit-form";
import { Alert } from "./alert";
type Props = {
userId: string;
loginName?: string;
sessionId?: string;
requestId?: string;
organization?: string;
loginSettings: LoginSettings;
userMethods: AuthenticationMethodType[];
checkAfter: boolean;
phoneVerified: boolean;
emailVerified: boolean;
force: boolean;
};
export function ChooseSecondFactorToSetup({
userId,
loginName,
sessionId,
requestId,
organization,
loginSettings,
userMethods,
checkAfter,
phoneVerified,
emailVerified,
force,
}: Props) {
const router = useRouter();
const params = new URLSearchParams({});
const [error, setError] = useState<string>("");
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
if (loginName) {
params.append("loginName", loginName);
}
if (sessionId) {
params.append("sessionId", sessionId);
}
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);
}
if (checkAfter) {
params.append("checkAfter", "true");
}
return (
<>
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
<div className="grid w-full grid-cols-1 gap-5 pt-4">
{loginSettings.secondFactors.map((factor) => {
switch (factor) {
case SecondFactorType.OTP:
return TOTP(userMethods.includes(AuthenticationMethodType.TOTP), "/otp/time-based/set?" + params);
case SecondFactorType.U2F:
return U2F(userMethods.includes(AuthenticationMethodType.U2F), "/u2f/set?" + params);
case SecondFactorType.OTP_EMAIL:
return (
emailVerified && EMAIL(userMethods.includes(AuthenticationMethodType.OTP_EMAIL), "/otp/email/set?" + params)
);
case SecondFactorType.OTP_SMS:
return phoneVerified && SMS(userMethods.includes(AuthenticationMethodType.OTP_SMS), "/otp/sms/set?" + params);
default:
return null;
}
})}
</div>
{!force && (
<button
className="text-sm transition-all hover:text-primary-light-500 dark:hover:text-primary-dark-500"
onClick={async () => {
const skipResponse = await skipMFAAndContinueWithNextUrl({
userId,
loginName,
sessionId,
organization,
requestId,
});
handleServerActionResponse(skipResponse, router, setSamlData, setError);
}}
type="button"
data-testid="reset-button"
>
<Translated i18nKey="set.skip" namespace="mfa" />
</button>
)}
{error && (
<div className="py-4" data-testid="error">
<Alert>{error}</Alert>
</div>
)}
</>
);
}