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

3.0 KiB

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.