Files
zitadel/apps/login/AGENTS.md
T
Max PeintnerandGitHub d488ecb07f fix(login): don't prepend base path to absolute IdP URL on login_hint redirect (#12610)
# 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.
2026-08-17 15:17:08 +02:00

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.