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]>
This commit is contained in:
Max Peintner
2026-02-24 11:02:29 +03:00
committed by GitHub
co-authored by Copilot
parent 87aae1b5fc
commit d27833e1bc
22 changed files with 267 additions and 53 deletions
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, test, vi, beforeEach } from "vitest";
import { isSafeRedirectUri } from "./client-utils";
describe("isSafeRedirectUri", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should reject empty uri", async () => {
expect(await isSafeRedirectUri("")).toBe(false);
expect(await isSafeRedirectUri(undefined as unknown as string)).toBe(false);
});
test("should allow relative paths", async () => {
expect(await isSafeRedirectUri("/dashboard")).toBe(true);
expect(await isSafeRedirectUri("/ui/console")).toBe(true);
expect(await isSafeRedirectUri("/")).toBe(true);
});
test("should reject protocol-relative paths", async () => {
expect(await isSafeRedirectUri("//evil.com")).toBe(false);
expect(await isSafeRedirectUri("\\\\evil.com")).toBe(false);
});
test("should allow absolute URLs matching the current host", async () => {
expect(await isSafeRedirectUri("https://my-zitadel.com/console")).toBe(true);
});
test("should allow absolute URLs to external domains", async () => {
expect(await isSafeRedirectUri("https://evil.com")).toBe(true);
expect(await isSafeRedirectUri("https://settings.com/dashboard")).toBe(true);
});
test("should reject non-http(s) protocols", async () => {
expect(await isSafeRedirectUri("javascript:alert(1)")).toBe(false);
expect(await isSafeRedirectUri("data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==")).toBe(false);
expect(await isSafeRedirectUri("file:///etc/passwd")).toBe(false);
});
test("should gracefully reject invalid, unparsable URLs that aren't relative", async () => {
expect(await isSafeRedirectUri("not-a-valid-url")).toBe(false);
expect(await isSafeRedirectUri("http://%")).toBe(false);
});
});