mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-27 17:52:14 +00:00
# Which Problems Are Solved When an OIDC authorize request carries a `login_hint` that resolves via domain discovery to an organization configured to auto-redirect to a single external IdP, Login V2's server-side flow initiation returns a malformed `Location` header: the login base path is concatenated with the absolute IdP authorize URL without a separator (e.g. `https://<instance>/ui/v2/loginhttps://login.microsoftonline.com/...`). The flow ends in `{"code":5,"message":"Not Found"}` and the IdP is never contacted. The same IdP works when the user submits the login name interactively, because the client-side handler distinguishes external URLs; only the server-side `login_hint` fast-path introduced in #12431 is affected. Additionally, the same fast-path only handled the `redirect` response shape from `sendLoginname`. When the discovered organization's IdP is SAML with POST binding, `sendLoginname` returns `samlData` (form fields instead of a URL), which was silently ignored — the user fell back to the prefilled `/loginname` screen instead of being signed in silently, unlike the client-side flow, which auto-submits the SAML AuthnRequest form. # How the Problems Are Solved `resolveLoginHint` passed every redirect returned by `sendLoginname` through `constructUrl`, which unconditionally prepends `NEXT_PUBLIC_BASE_PATH` and assumes a relative path. It now checks `isExternalUrl` first: absolute URLs are validated with `isSafeRedirectUri` (blocking `javascript:`/`data:`/etc., falling back to `/loginname` if unsafe) and redirected as-is, while relative paths keep being resolved against the base path — matching the handling already used in the idp-scope branch of `handleOIDCFlowInitiation` and in the client-side `handleServerActionResponse`. `resolveLoginHint` also handles the `samlData` response shape now: after validating the target URL with `isSafeRedirectUri`, it responds with the same auto-submit HTML form used by the existing SAML flows, so a `login_hint` resolving to a POST-binding SAML IdP completes silently as well. Since this form was previously duplicated inline three times in `flow-initiation.ts`, it is extracted into a shared `buildAutoSubmitFormResponse` helper used by all call sites. Regression tests cover the absolute-URL redirect (fails on the previous code with the exact malformed URL), the SAML POST auto-submit response, and the unsafe-scheme fallbacks for both shapes.
32 lines
3.0 KiB
Markdown
32 lines
3.0 KiB
Markdown
# ZITADEL Login App Guide for AI Agents
|
|
|
|
## Context
|
|
The **Login App** (`apps/login`) provides the user interface for authentication flows (Login, Register, MFA, etc.). It is built with Next.js and React.
|
|
|
|
## Key Technology
|
|
- **Framework**: Next.js (React).
|
|
- **Styling**: TailwindCSS, configured via `apps/login/tailwind.config.mjs`.
|
|
- **Data Fetching**: Primarily server-side interaction with ZITADEL APIs via `@zitadel/client` or direct gRPC calls where applicable.
|
|
- **Language**: TypeScript.
|
|
|
|
## Architecture & Conventions
|
|
- **Routing**: Uses the Next.js App Router (routes are defined under `src/app/`).
|
|
- **Composability**: Components should be small and reusable.
|
|
- **State**: Critical authentication state is often managed via URL parameters (Auth Requests) and cookies/sessions.
|
|
- **Scope Rule**: For shared API typings and client behavior, also read `packages/AGENTS.md` and `proto/AGENTS.md`.
|
|
|
|
## Base Path & Redirects (critical — read before touching any redirect logic)
|
|
- **The login app is served under a non-root base path in production.** ZITADEL Cloud and the official container image are built with `NEXT_PUBLIC_BASE_PATH=/ui/v2/login` (see `apps/login/.env`; the value is inlined at build time by Next.js). Never assume the app lives at the domain root.
|
|
- **`constructUrl()` (`src/lib/service-url.ts`) prepends the base path and must ONLY ever receive root-relative paths** (`/loginname`, `/password?...`). Passing an absolute URL glues the base path onto it and produces a broken redirect like `https://<host>/ui/v2/loginhttps://login.microsoftonline.com/...`.
|
|
- **Some redirect targets ARE absolute URLs**: IdP authorize URLs returned by `startIdentityProviderFlow`, OIDC callback URLs from `createCallback`, and SAML endpoints from `createResponse`. When handling a redirect value that could be absolute, branch on `isExternalUrl()` first, validate absolute URLs with `isSafeRedirectUri()`, and pass them to `NextResponse.redirect` (server) / `window.location.href` (client) untouched. Canonical pattern: `resolveLoginHint` in `src/lib/server/flow-initiation.ts` and `handleServerActionResponse` in `src/lib/client-utils.ts`.
|
|
- **An empty base path masks this whole bug class**: with `NEXT_PUBLIC_BASE_PATH` unset, `new URL("" + absoluteUrl, origin)` still parses the absolute URL correctly, so local setups without the base path won't reproduce base-path bugs. Always test redirect changes with `NEXT_PUBLIC_BASE_PATH=/ui/v2/login` set, and verify the raw `Location` header (e.g. `curl -I`), not just browser behavior.
|
|
|
|
## Verified Nx Targets
|
|
- **Dev Server**: `pnpm nx run @zitadel/login:dev`
|
|
- **Build**: `pnpm nx run @zitadel/login:build`
|
|
- **Lint**: `pnpm nx run @zitadel/login:lint`
|
|
- **Test (all)**: `pnpm nx run @zitadel/login:test`
|
|
- **Test (unit)**: `pnpm nx run @zitadel/login:test-unit`
|
|
- **Test (integration)**: `pnpm nx run @zitadel/login:test-integration`
|
|
- **Pack (Docker)**: `pnpm nx run @zitadel/login:pack` — builds a local Docker image `zitadel/zitadel-login:local`. Requires Docker daemon.
|