# Which Problems Are Solved
Fixes i18n consistency and adds HU translations for login v2
# How the Problems Are Solved
- Updated i18n files and added hu.json
- Moved all translations to v2-default.json
---------
Co-authored-by: Dobos Zoltán <[email protected]>
Co-authored-by: Liam Neville <[email protected]>
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->
# Which Problems Are Solved
The `sessions` cookie lifetime is independent of Zitadel's server-side
session validity, so a cookie can reference an invalid session. In that
case `getSession` throws `not_found` (QUERY-SFeaa).
- `loadMostRecentSession` lets that error propagate instead of treating
it as "no session", crashing the server render of every page that calls
it without its own guard.
- Most visibly, the "forgot password" flow landing on `/password/set`
throws `[not_found] Session does not exist (QUERY-SFeaa)` (gRPC code 5,
HTTP 404) and fails to render.
- 12 of the 16 call sites are unguarded and share this crash.
# How the Problems Are Solved
Catch the error in `loadMostRecentSession` and return `undefined`,
unifying "cookie references a dropped session" with the existing "no
cookie" case. Every call site already handles an undefined session,
either with a graceful fallback or by throwing its own explicit error.
```ts
return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token })
.then((resp: GetSessionResponse) => resp.session)
.catch((error) => {
console.warn("[Session] Could not load most recent session", error);
return undefined;
});
```
All `loadMostRecentSession` call sites — **Previous** = behaviour when
the cookie referenced an invalid session; **Change** = what this PR does
to that call site:
| # | Call site | Change in this PR | Previous behaviour (invalid
session) | New behaviour |
|---|-----------|-------------------|--------------------------------------|---------------|
| — | `lib/session.ts` — `loadMostRecentSession` | **Added `.catch` →
`undefined`** (core fix) | Threw `not_found` to caller | Resolves to
`undefined` |
| 1 | `password/set/page.tsx` | none | **Crash** (the reported bug) |
Falls back to login-name flow |
| 2 | `password/change/page.tsx` | none | **Crash** | `undefined` →
`failedLoading` alert if no user |
| 3 | `passkey/set/page.tsx` | none | **Crash** | `undefined` → `userId`
fallback / `unknownContext` |
| 4 | `otp/[method]/page.tsx` | none | **Crash** | `undefined` →
`unknownContext` alert |
| 5 | `otp/[method]/set/page.tsx` | none | **Crash** (raw
`ConnectError`) | `undefined` → own `throw "No session found"` |
| 6 | `mfa/page.tsx` | none | **Crash** | `undefined` →
`verify.noResults` alert |
| 7 | `mfa/set/page.tsx` | none | **Crash** (raw `ConnectError`) |
`undefined` → own `throw "Could not get user id from session"` |
| 8 | `authenticator/set/page.tsx` | none | **Crash** (raw
`ConnectError`) | `undefined` → own throw / `unknownContext` |
| 9 | `signedin/page.tsx` | none | **Crash** | `undefined` → renders
with `loginName` fallback |
| 10 | `u2f/set/page.tsx` | none | **Crash** | `undefined` →
`unknownContext` alert |
| 11 | `u2f/page.tsx` | none | **Crash** | `undefined` → renders on
`loginName` |
| 12 | `lib/server/verify.ts` — `verifyTOTP` | none | **Crash** (raw
`ConnectError`) | `undefined` → own `throw "No user id found in
session."` |
| 13 | `password/page.tsx` | **Removed redundant `try/catch`** | Caught
locally → `undefined` | `undefined` — unchanged |
| 14 | `verify/page.tsx` | **Removed redundant `.catch`** ¹ | Caught
locally → `undefined` | `undefined` — unchanged |
| 15 | `verify/success/page.tsx` | **Removed redundant `.catch`** |
Caught locally → `undefined` | `undefined` — unchanged |
| 16 | `passkey/page.tsx` | **Removed redundant `.catch`** (login-name
path) | Caught locally → `undefined` | `undefined` — unchanged |
Rows 1–12 were unguarded and crashed; the central change fixes them.
Rows 13–16 already handled the error locally.
# Additional Changes
- This makes the four callers that wrapped the call in their own
try/catch or `.catch` redundant, so remove that now-dead handling
(password, verify, verify/success, passkey).
- Add unit tests covering `loadMostRecentSession`: no cookie, success,
and the stale-cookie rejection path.
¹ `verify/page.tsx`'s `.catch` also eagerly fetched `loginSettings`;
that is already covered by the page's own lazy fetch, so behaviour is
unchanged (one cosmetic edge case under `ignoreUnknownUsernames` with
both `userId` and `loginName` present — no functional impact).
# Additional Context
- Session *expiry* alone does not trigger this: expired rows are not
removed from the session projection, so `getSession` still returns them.
The `not_found` specifically means the session was **removed**
server-side (logout, admin/API termination, or a user/org/instance
cascade) while the browser still held the cookie.
- Mirrors the pattern upstream already applies inline in
`verify/page.tsx` (*"ignore error, as we might not have a session
yet"*), lifted to the shared function.
- **Related issue (not in this PR):** the sibling `loadSessionById`
helpers on the `?sessionId=` path (`passkey`, `otp`, `mfa`, `mfa/set`,
`signedin`, `authenticator/set`, `u2f`) call `getSession` directly and
share the same defect. Lower practical exposure (short-lived in-flow
URLs; a clean logout clears the cookie), so a follow-up PR should lift
those into a shared `loadSessionById` in `lib/session.ts`.
- Prior art: upstream added an equivalent guard to verify/page.tsx in
#11130, but the other call sites were never given the same treatment —
this PR resolves that inconsistency at the source.
Error trace:
```
{"level":"error","message":"⨯ Error [ConnectError]: [not_found] Session does not exist (QUERY-SFeaa)\n at R (.next/server/chunks/ssr/_0uqqxk7._.js:3:25490)\n at <unknown> (.next/server/chunks/ssr/_0uqqxk7._.js:3:26477)\n at next (.next/server/chunks/ssr/_0uqqxk7._.js:3:68112)\n at async (.next/server/chunks/ssr/_0uqqxk7._.js:10:31326)\n at async (.next/server/chunks/ssr/_0uqqxk7._.js:10:33734)\n at async Object.unary (.next/server/chunks/ssr/_0uqqxk7._.js:3:66838)\n at async Object.getSession (.next/server/chunks/ssr/_0uqqxk7._.js:3:72371)\n at async o (.next/server/chunks/ssr/[root-of-the-server]__0msfcq_._.js:1:7440) {\n rawMessage: 'Session does not exist (QUERY-SFeaa)',\n code: 5,\n metadata: Headers {\n 'accept-encoding': 'gzip',\n 'alt-svc': 'h3=\":443\"; ma=2592000',\n 'content-length': '240',\n 'content-type': 'application/json',\n date: 'Wed, 08 Jul 2026 09:06:09 GMT',\n 'strict-transport-security': 'max-age=63072000; includeSubDomains; preload',\n vary: 'Origin',\n },\n details: [\n {\n type: 'zitadel.v1.ErrorDetail',\n value: [Uint8Array],\n debug: [Object]\n }\n ],\n cause: undefined,\n httpStatus: 404,\n isUserError: true,\n digest: '634020356'\n}","timestamp":"2026-07-08T09:06:09.969Z"}
```
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Liam Neville <[email protected]>
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->
# Which Problems Are Solved
- On Login V2 after a user sets their password during the invite or
reset-password flows and clicks continue,
there is a few seconds delay before the redirect occurs because no
loading spinner is visible on the button
during this delay users assume nothing happened and click continue
again.
- Clicking the button again starts a duplicate form submission. Since
the first request was already fired the second request triggers an error
right before the redirect from the first request completion creating a
bad user experience
# How the Problems Are Solved
- Kept the `loading` state active `true` throughout the lifeline of the
request which includes the 2-second database consistency delay and the
subsequent `sendPassword` verification request.
- Added `setLoading(false)` calls only in early-return error handling
blocks and at the end of the
redirection flow ensuring the button remains disabled and shows the
spinner until the redirect happens.
# Additional Changes
- None
# Additional Context
- Solves #12416
Co-authored-by: Max Peintner <[email protected]>
Closes#12024
# Which Problems Are Solved
When the login flow is started with an
`urn:zitadel:iam:org:domain:primary:{domain}` scope, the resolved domain
suffix is lost if the user navigates through the account chooser (e.g.
clicking "Use another account"). This causes the login to fail with
"User not found" because the organization can no longer be resolved.
Additionally, the `hideLoginNameSuffix` branding setting was never
respected by the login app.
# How the Problems Are Solved
- Forward the `orgDomain` parameter through `gotoAccounts()` and the
`/accounts` page, so it survives navigation to `/loginname` — the same
way `organization` and `requestId` are already forwarded.
- Respect `BrandingSettings.hideLoginNameSuffix`: when enabled, the
`@domain.com` suffix is hidden from the input UI but still used
internally for user search.
---------
Co-authored-by: Livio Spring <[email protected]>
# Which Problems Are Solved
1. **Secondary button hover effect** was rendering a solid white/gray
background instead of a subtle transparent overlay. The legacy
`bg-opacity-*` utility (e.g. `hover:bg-gray-500 hover:bg-opacity-20`)
doesn't compose with `bg-*` in Tailwind v4 — the opacity is ignored,
leaving a solid color. The same issue affected `ring-opacity-*` and
`border-opacity-*` across other components.
2. **Checkbox checkmark color** was hardcoded to white (`fill='white'`
in the SVG), ignoring the theme's primary contrast color. On themes with
a light primary color, the white checkmark was invisible.
# How the Problems Are Solved
1. Migrated all legacy opacity utilities to the Tailwind v4 slash
syntax:
- `hover:bg-gray-500 hover:bg-opacity-20` → `hover:bg-gray-500/20`
- `ring-primary-light-500 ring-opacity-60` → `ring-primary-light-500/60`
- `focus:ring-opacity-50` + `focus:ring-indigo-200` →
`focus:ring-indigo-200/50`
- Removed redundant `border-opacity-20` where `border-black/10` was
already applied
2. Replaced the static `background-image` checkbox SVG with a
`mask-image` + `::after` pseudo-element approach. The checkmark color
now uses `var(--theme-light-primary-contrast-500)` /
`var(--theme-dark-primary-contrast-500)`, so it dynamically follows the
theme's contrast color.
---------
Co-authored-by: Livio Spring <[email protected]>
Closes#11946
# Which Problems Are Solved
- OIDC redirect is broken when `Prompt.LOGIN` + `loginHint` is used —
the user lands on `/signedin` instead of being redirected to the OIDC
client's callback URL (fixes#11946)
# How the Problems Are Solved
- In `handleOIDCFlowInitiation`, the `Prompt.LOGIN` + `loginHint` code
path was passing `authRequest.id` (raw ID without `oidc_` prefix) to
`sendLoginname`. Without the prefix, `completeFlowOrGetUrl` does not
recognize the flow as OIDC and falls through to the "Regular flow" path,
redirecting to `/signedin` instead of calling `createCallback`. Fixed by
using the `requestId` parameter which already carries the `oidc_`
prefix.
# Additional Changes
- Standardized all request ID references in `handleOIDCFlowInitiation`
to consistently use the `requestId` parameter instead of reconstructing
it with `` `oidc_${authRequest.id}` ``. This eliminates the
inconsistency that caused the bug and prevents similar issues in the
future.
- Removed a redundant `if (authRequest.id)` guard in the LDAP redirect
path, since `requestId` is always present as a required parameter.
- Added regression tests verifying that `sendLoginname` receives the
`requestId` with the `oidc_` prefix in the `Prompt.LOGIN` + `loginHint`
code path.
Closes#11914
# Which Problems Are Solved
When an OIDC auth request includes an organization scope
(`urn:zitadel:iam:org:id:{id}` or
`urn:zitadel:iam:org:domain:primary:{domain}`), users with existing
browser sessions from *other* organizations were shown an empty Account
Selection page with no selectable accounts. The only option was to click
"Add another account", adding a confusing and unnecessary extra step.
This happened because the `/login` route checked `sessions.length` (all
browser sessions, unfiltered) to decide whether to enter the "reuse
existing session" branch. Inside that branch, `findValidSession`
correctly filtered by organization and returned no match — but the
fallback redirected to `/accounts`, which also filters by org and
rendered empty.
# How the Problems Are Solved
Before deciding to redirect to `/accounts`, pre-filter sessions by the
requested organization using the same logic already used by the accounts
page and `findValidSession`. If no sessions are eligible for the target
organization, redirect directly to `/loginname` instead.
This applies to both the **default prompt** and
**`prompt=select_account`** branches. For `select_account`, this matches
the behavior of major OIDC providers like Google and Microsoft, which
skip the account chooser and go straight to the login input when there
are no sessions to select from. The OIDC spec describes `select_account`
as enabling selection "amongst multiple accounts that they might have
current sessions for" — showing an empty picker serves no purpose.
---------
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
# Which Problems Are Solved
When the registerUser server action failed during registration, all
errors from addHumanUser, createSessionWithRetry, and getUserByID
propagated as uncaught exceptions to the client component's generic
catch block, which always showed the same "Could not register user"
message. This made it impossible to distinguish between user creation
failures, session creation failures, and user lookup failures — both for
end users and in server logs.
# How the Problems Are Solved
Added .catch() handlers on the three gRPC calls in registerUser that log
the actual error and return null, allowing the existing null-check guard
clauses to return step-specific error messages (couldNotCreateUser,
couldNotCreateSession, userNotFound) through handleServerActionResponse.
This gives users more specific feedback and provides server-side logging
to narrow down the root cause of flaky signup failures.
# Which Problems Are Solved
- Native apps (mobile/desktop) use custom protocol schemes (e.g.
`myapp://callback`, `io.zitadel.app://auth`) as OAuth/OIDC redirect
URIs. The previous redirect-safety check in the login UI only allowed
`http:`/`https:` absolute URLs, so these custom-scheme callbacks were
wrongly rejected and native app login flows were broken.
- Even for valid external redirects, `router.push()` triggers an RSC
prefetch `fetch()` that is blocked by the CSP `connect-src 'self'`
directive for non-same-origin URLs, so navigation to
external/custom-protocol targets failed.
- SAML and OIDC callback URLs returned from the backend were not
consistently validated before being used for redirects/POSTs across the
various flow entry points.
# How the Problems Are Solved
- Reworked `isSafeRedirectUri` to use `@braintree/sanitize-url` instead
of a strict `http/https`-only allowlist. Custom protocol schemes are now
permitted, while dangerous schemes (`javascript:`, `data:`, plus an
explicit block of `file:`, `blob:`, `about:`) are still rejected.
- Added `isExternalUrl` helper to distinguish internal relative paths
(`/...`) from external/custom-protocol targets.
- In `handleServerActionResponse`, external/custom-protocol redirects
now navigate via `window.location.href` (full navigation) instead of
`router.push()`, avoiding the CSP-blocked RSC prefetch. Internal paths
continue to use `router.push()`.
- Added consistent redirect-URI validation for SAML and OIDC callbacks
across all relevant entry points (`oidc.ts`, `saml.ts`,
`server/flow-initiation.ts` including the `prompt=none` path, and the
SAML `samlData` POST case), blocking unsafe URLs with a clear warning
and error response.
# Additional Changes
- Added `@braintree/sanitize-url` (`^7.1.2`) dependency to `apps/login`
(`package.json` / `pnpm-lock.yaml`).
- Added unit tests for `isExternalUrl` covering relative paths, absolute
HTTP/HTTPS URLs, custom protocol schemes, and protocol-relative URLs.
- Annotated the intentional `window.location.href` navigation with a
CodeQL suppression comment, documenting that the URL is validated by
`isSafeRedirectUri` beforehand.
- Minor formatting cleanup in `console` JWT provider component.
# Additional Context
- This restores behavior that previously worked for native apps but
regressed when redirect validation was tightened to `http`/`https` only.
---------
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
Login v2 uses the `users.v2.ListUsers`-endpoint to get a user by login
name. This query had an inefficient `WHERE`-clause.
# How the Problems Are Solved
Update the view and use a specific clause for this query.
# Additional information
Added in https://github.com/zitadel/zitadel/pull/10475
---------
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
# Which Problems Are Solved
This fixes a critical bug where initial verification emails (or invite
codes) were occasionally sent twice, invalidating the first code and
confusing users.
Previously, the initial verification email was triggered via a
`send=true` URL parameter executing inside a frontend useEffect exactly
when the `/verify` page mounted. This was fragile and prone to race
conditions caused by component remounts or partial hydration.
# How the Problems Are Solved
- Removed `send=true` from URL state and ripped out the doSend effect in
`VerifyForm.tsx`.
- Shifted execution strictly to the Next.js server. The email is now
automatically dispatched via await `initialSendVerification(...)` during
the POST requests (acting over `sendLoginname`, `register`, `password`,
`passkeys`, and `idp`).
- The login flow is now idempotent and robust against unintended
frontend re-renders.
- Refactored `checkEmailVerification()` to be async and updated the
associated unit-test coverage (all tests passing).
# Which Problems Are Solved
When using IDP auto-creation, the addHuman() call would fail if the IDP
didn't provide required profile fields (givenName or familyName),
resulting in a poor user experience.
# How the Problems Are Solved
Added validation before auto-creation to check if required profile
fields are present. If givenName or familyName is missing, users are now
redirected to the complete-registration page where they can manually
provide the missing information.
- Added profile field validation in CASE 4 (auto-creation) of
processIDPCallback
- Redirect to /idp/{provider}/complete-registration when required fields
are missing
- Pre-fill any available user data in the registration form
---------
Co-authored-by: David Skewis <[email protected]>
Co-authored-by: Florian Forster <[email protected]>
# Which Problems Are Solved
This improves the contrast of the “Processing authentication...” message
shown while Login V2 processes the IDP authentication flow.
The previous style used text-gray-600 without a dark-mode variant. This
color is hard-coded in the component and is not affected by the branding
colors, so users may see low contrast depending on the active
theme/background.
This PR updates the message text to use a higher-contrast light/dark
color pair.
---------
Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
Closes#12182
# Which Problems Are Solved
Generic IDP buttons (OIDC, SAML, LDAP, JWT) used pl-20 to approximate
icon offset alignment, but since they have no icon, the text appeared
misaligned compared to branded IDPs (Google, Microsoft, Apple).
# How the Problems Are Solved
Replaced the left-padding hack with centered text so generic IDP names
display cleanly within the button.
before:
<img width="392" height="290" alt="Screenshot 2026-05-28 at 12 03 21"
src="https://github.com/user-attachments/assets/64c4e8eb-caec-4742-b61c-e3b0c5093dd7"
/>
after:
<img width="392" height="289" alt="Screenshot 2026-05-28 at 12 07 27"
src="https://github.com/user-attachments/assets/d1faf1f4-0aab-475f-8801-15b461e15da4"
/>
Closes#11200
# Which Problems Are Solved
Custom fonts uploaded via the branding/label policy were not supported.
# How the Problems Are Solved
- The login now correctly applies the branding settings, based on the
organization context
- Use the absolute `fontUrl` from the branding API directly in the
`@font-face` `src`, matching how logo and icon assets are already loaded
via absolute URLs.
- Add the Zitadel service URL to the `font-src` CSP directive so the
browser permits loading the cross-origin font.
# Additional Changes
- Updated CSP tests to reflect the new `font-src` behavior.
# Which Problems Are Solved
The local `isSessionValid` in `passkeys.ts` only checked `password` and
`webAuthN` factors, so sessions authenticated via an external IDP
(`intent` factor) were treated as invalid. This caused a "You have to
authenticate" error when redirecting to `/passkey/set` after IDP login.
# How the Problems Are Solved
Added `session.factors.intent.verifiedAt` to the validity check,
consistent with the canonical `isSessionValid` in `session.ts`.
After user registration, the backend projections may not be up to date
yet when the Login UI immediately tries to create a session. This
results in a `QUERY-Dfbg2` ("User could not be found") error even though
the user was created successfully.
This adds retry logic with backoff (500ms/1s/2s, up to 3 attempts)
around `createSessionAndUpdateCookie` in the registration flow. Only
`NotFound` errors are retried — other errors are thrown immediately.
Closes#12173
---------
Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
# Which Problems Are Solved
When MFA is enforced with only Email OTP as a second factor, new users
hit a dead end on the MFA setup screen. The `ChooseSecondFactorToSetup`
component hides Email OTP when `emailVerified` is `false`, and if email
verification was skipped earlier in the flow (e.g. `EMAIL_VERIFICATION`
env not set to `"true"`), no MFA options are visible and the user cannot
proceed.
# How the Problems Are Solved
In the MFA setup server component (`mfa/set/page.tsx`), before
rendering, detect when forced MFA has no visible second factors because
all configured factors require verification the user hasn't completed.
When Email OTP is configured but the user's email is unverified,
redirect to `/verify` with `send=true` so a verification code is sent
immediately.
After the user verifies their email, the existing flow in `verify.ts`
calls `checkMFAFactors` which redirects back to `/mfa/set` — where Email
OTP is now visible since `emailVerified` is `true`.
The redirect pattern follows the existing precedent in
`authenticator/set/page.tsx`.
# Additional Changes
None
# Additional Context
- Closes#11536
---------
Co-authored-by: Max Peintner <[email protected]>
Closes#12125
# Which Problems Are Solved
The proxy middleware (proxy.ts) did not apply `CUSTOM_REQUEST_HEADERS`
to rewritten requests (/.well-known/*, /oauth/*, /oidc/*, etc.). When
`ZITADEL_API_URL` points to an internal service name, the Host header on
proxied requests remained the internal name instead of the configured
public domain, causing Errors.Instance.NotFound.
# How the Problems Are Solved
The other two outgoing request paths — the connectRPC transport and the
security-settings fetch — already applied these headers. This adds the
same applyCustomHeaders() call to the proxy path.
Co-authored-by: Ramon <[email protected]>
# Which Problems Are Solved
- Login V2 signup does not resolve the `{{.Lang}}` placeholder in ToS,
Privacy Policy, and help links.
- Clicking those links on the registration page can open URL-encoded
placeholders like `%7B%7B.Lang%7D%7D` instead of the active
language-specific page.
# How the Problems Are Solved
- Adds a small login utility to resolve `{{.Lang}}` placeholders against
the active Login V2 locale.
- Updates the signup legal acceptance component to use the resolved
links before rendering them.
- Applies the same resolution to the optional help link shown alongside
the legal acceptance text.
# Additional Changes
- Adds unit tests for the link placeholder resolver.
- Adds a component test covering localized ToS, Privacy Policy, and help
links on the signup page.
# Additional Context
- Closes#11907
---------
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
- Logo and heading overlap on login pages due to a -mb-4 negative margin
on the logo container
- The /login route returns a 500 when `listSessions` fails (e.g. stale
session cookies), because loadSessions has no error handling.
- The proxy logs `"fetch() returned undefined"` because
`fetchIframeOrigins` returns undefined, which lru-cache's forceFetch
treats as a fetch failure.
# How the Problems Are Solved
- Remove `-mb-4` from the logo container in DynamicTheme.
- Wrap `loadSessions` in a try/catch — failures fall through as empty
sessions instead of crashing.
- Return `null` instead of `undefined` from `fetchIframeOrigins` so
lru-cache caches it normally.
---------
Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: conblem <[email protected]>
# Which Problems Are Solved
1. SSL/TLS handshake failures on Cloud Run (middleware self-loopback)
PR #11903 changed the middleware (proxy.ts) to fetch security settings
via fetch(origin + "/security") on every request, including non-proxy
routes like /login. On Cloud Run, this self-loopback goes through the
Google Front End (GFE) load balancer, causing intermittent SSL
routines::record layer failure errors that were tenant-consistent.
2. The ClassifiedConnectError introduced in #11926 set this.name =
"ClassifiedConnectError", which broke ConnectError's custom
Symbol.hasInstance duck-typing check (v.name === "ConnectError"). This
caused ConnectError.from() inside the connectRPC transport's abort
handler to re-wrap classified errors as new ConnectError instances with
Code.Unknown — losing the original gRPC error code and all
classification metadata.
# How the Problems Are Solved
Middleware: eliminate self-loopback fetch:
- Replaced the fetch(origin + "/security") self-loopback with a direct
fetch to ZITADEL_API_URL using the Connect protocol (POST + JSON),
bypassing the load balancer entirely
- Security settings are cached in-memory with a 1-hour TTL per instance
host
- Extracted the fetching/caching logic into
src/lib/server/security-settings.ts
Removed the now-unused /security API route (src/app/security/route.ts)
- CSP headers with iframe origins are now applied to all routes without
any loopback
Error classification interceptor: fixed
- Keep this.name = "ConnectError" in ClassifiedConnectError so the
duck-typing Symbol.hasInstance check passes
- The branded Symbol.for check via isClassifiedError() still correctly
distinguishes the subclass
- Remove redundant Object.setPrototypeOf call (the super constructor
already handles it via new.target)
- Replace remaining instanceof ConnectError checks with
isClassifiedError() in setUserPassword and checkSessionAndSetPassword
# Which Problems Are Solved
The in-memory SWR promiseCache in zitadel.ts used keys like
getBrandingSettings-${org} that had no instance identifier. In
multi-tenant mode, where a single Next.js process serves multiple
instances, an instance A's cached settings without org context
(branding, languages, settings, etc.) could be served to Instance B
request without org context. The cache now additionally is bound to 100
entries by default (configurable via maxSize in `API_CACHE_CONFIG`).
When capacity is exceeded, expired entries are swept first
It additionally replaces the hand-rolled PromiseCache (Map-based, FIFO
eviction) with lru-cache, leveraging its built-in fetchMethod for
stale-while-revalidate, request deduplication, and true LRU eviction.
# How the Problems Are Solved
Added an instanceCacheKey() helpe that prefixes every cache key with
serviceConfig.instanceHost in addition to the org context if available.
---------
Co-authored-by: Ramon <[email protected]>
# Which Problems Are Solved
When a user needed to verify their email during an OIDC login flow, the
requestId was lost, preventing the flow from completing with a redirect
back to the relying party.
# How the Problems Are Solved
In `sendVerification()`, the session cookie lookup used `"loginName" in
command` to decide whether to fall back to `user.preferredLoginName`.
Since the `loginName` property key always exists in the command object
(even when its value is undefined), the fallback was never triggered.
This caused the lookup to search for loginName === undefined, finding no
session — and ultimately redirecting to a dead-end success page.
- /authenticator/set redirect: Added missing requestId to the URL params
when redirecting users who need to set up a primary auth method.
- /verify/success page: Added a "Continue" button that re-enters the
login flow with requestId preserved, handling the edge case where no
session cookie exists (e.g. email link opened in a different browser).
## What does this PR do?
Adds complete **Portuguese (pt)** locale translations for the Login V2
interface, making it the 14th supported language.
### Problem
The Login V2 component currently supports 13 languages but does not
include Portuguese — one of the most spoken languages in the world
(~260M native speakers). This was reported in #11782.
### What we found
- The Login V2 translations are stored as JSON files under
`apps/login/locales/`
- Available languages are registered in the `LANGS` array in
`apps/login/src/lib/i18n.ts`
- Portuguese (`pt.json`) was missing from both the locales directory and
the `LANGS` array
### What we did
1. Created `apps/login/locales/pt.json` with complete Portuguese
translations for all login screens (loginname, password, register, MFA,
passkeys, OTP, sessions, errors, etc.)
2. Added `{ name: "Português", code: "pt" }` to the `LANGS` array in
`apps/login/src/lib/i18n.ts`
### Translation methodology
- Based on the existing `en.json` and `es.json` as reference
- Follows Brazilian Portuguese (pt-BR) conventions, which is understood
by all Portuguese-speaking countries
- All keys translated — no fallbacks to English needed
### Files changed
| File | Change |
|------|--------|
| `apps/login/locales/pt.json` | **New** — Complete Portuguese
translations |
| `apps/login/src/lib/i18n.ts` | **Modified** — Added `pt` entry to
`LANGS` array |
### Testing
We built and deployed this change from source (tag v4.12.1) to a
production Zitadel instance:
- ✅ Portuguese appears in the language dropdown
- ✅ All login screens display correct Portuguese translations
- ✅ Language selection persists across pages
- ✅ OIDC flow works correctly with Portuguese selected
- ✅ No build warnings or errors related to the translation
Closes#11782
Co-authored-by: rofilho <[email protected]>
# Which Problems Are Solved
This pr updates major and minor dependencies and is the first step on
getting our dependabot alerts cut down.
# How the Problems Are Solved
Depedency updates across the board eg:
- Upgrade Angular to v21
- Upgrade next.js to v16.2
- Upgrade tailwind to v4 in the login
- Upgrade vitest to v4 in the login
This is an uncompleted list refer to the changed files for a full
overview of all the updates.
# Additional Changes
Migrated all control flow in the console to the modern control flow
syntax.
Fixed the dependsOn setting for the @zitadel/login:test-unit nx target.
# Additional Context
- Closes https://github.com/zitadel/zitadel/issues/11274
---------
Co-authored-by: Max Peintner <[email protected]>
Closes#10929
# Which Problems Are Solved
When a human user without a primary authentication method enters their
email on the login screen, the login historically auto-sent an email
code (send=true) and redirected them to the `/verify` flow. If the user
was newly created via the API and already received an initial
verify-email, navigating to the login page would trigger a new invite
code, silently invalidating the code they received in their first email.
Additionally instead of resending the same type of email (invitation) a
regular email verification mail was sent
# How the Problems Are Solved
- Resend an invitation email instead of a email verification if a user
has no method set (still in invitation state)
- Conditional Code Sending: Updated `loginname.ts` to check
`humanUser?.email?.isVerified`. We only auto-send a new code
(`send=true`) if the user's email is already verified. Unverified users
will be redirected with `send=false`, allowing them to safely enter the
code they already have.
- UI State Fix: Fixed an issue in `verify/page.tsx` where the send URL
parameter was being checked directly as a string ("false" is truthy). By
using the properly evaluated doSend boolean, the "Code Sent" alert now
correctly hides itself when a new code is not explicitly sent.
- Translation Updates: Refined the codeSent messaging across all locales
to specify "A new code has been sent..." to provide better context to
the user when they do explicitly request a resend.
Closes#11721
# Which Problems Are Solved
The login app now correctly handles the themeMode from branding
settings:
- Hide toggle when themeMode is LIGHT or DARK — the theme is forced,
users cannot switch
- Show 3-option toggle (light / system / dark) when themeMode is AUTO or
UNSPECIFIED
# How the Problems Are Solved
- Introduced a `BrandingContext` to pass `themeMode` from `ThemeWrapper`
down to `ThemeSwitch`
# Additional Changes
- removed the CSP from next.config.ts to prevent a precedency issue
where the CSP was actually not applied
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->
# Which Problems Are Solved
- With Login V2 and EMAIL_VERIFICATION=true, opening the verify page
with send=true can trigger email code generation twice.
- The second code invalidates the first one, so users may receive two
emails where the first code is always invalid.
- This causes failed verification attempts and blocks onboarding for
affected users.
# How the Problems Are Solved
- Removed side-effectful email-code sending from direct server component
execution path.
- Ensured code sending is executed only once in a safe server-side flow,
so React Server Component re-fetches do not generate additional codes.
- Kept verify flow behavior intact while preventing duplicate code
invalidation.
# Additional Changes
- Added/updated logic around the verify flow to make repeated
render/re-fetch paths idempotent for email-code sending.
- Improved reliability of the Login V2 email verification step under App
Router navigation behavior.
# Additional Context
Fixes#11857
- Tested manually with Login V2 and EMAIL_VERIFICATION=true by
completing a new user registration flow to /verify?send=true.
- Verified that only one verification email is sent and the first
received code is valid (no duplicate invalidating code generated).
Related bug report with reproduction details:
[https://github.com/zitadel/zitadel/issues/11857](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
---------
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
This fixes an issue where resending a code for invite flow did not have
the same context it was initially created for
# How the Problems Are Solved
by passing down thre requestId, the context is preserved though a resend
Closes#11923
# Which Problems Are Solved
Client-side gRPC errors (e.g. NotFound, InvalidArgument,
PermissionDenied) were being surfaced as HTTP 500 Internal Server
Errors, causing false SRE alerts and poor UI feedback.
# How the Problems Are Solved
- New transport interceptor (`error-classification.ts`): Automatically
enriches every ConnectError with httpStatus and isUserError metadata via
a `ClassifiedConnectError` wrapper
- Route handler protection (`route.ts`, `flow-initiation.ts`): Catches
classified errors from `getAuthRequest` / `getSAMLRequest` and returns
correct HTTP status codes instead of 500
- Type safety: Replaced all magic number error.code === 9 checks with
typed Code.FailedPrecondition + instanceof ConnectError across
`oidc.ts`, `saml.ts`, `password.ts`, `zitadel.ts`
- Classification-aware logging (`session.ts`): Client/user errors log at
warn level, server errors at error level
- Observability (`otel.ts`): Spans now include error.is_user_error and
http.status_code attributes for alert filtering
# Additional Changes
Gitignore `next-env.d.ts`: This file is auto-generated by Next.js on
every next dev and next build invocation. The two modes write slightly
different import paths (.next/dev/types/ vs .next/types/), which causes
git diff --exit-code to fail in CI whenever a developer runs next dev
locally before committing. Since Next.js regenerates it automatically,
there's no need to track it
---------
Co-authored-by: Ramon <[email protected]>
Co-authored-by: Copilot <[email protected]>
# Which Problems Are Solved
Updated the project to use the latest versions of Next.js and React, and
implemented a SWR caching strategy for key settings API calls to improve
performance. Adjusted configuration to ensure compatibility with Next.js
16.
# How the Problems Are Solved
- Updated next to 16.1.6, react & react-dom to 19.2.4, and next-intl to
4.8.3.
- Implemented `API_CACHE_ENABLED` and `API_CACHE_CONFIG` (replacing
deprecated unstable_cacheLife) for:
- getBrandingSettings
- getSecuritySettings
- getPasswordComplexitySettings
- getLoginSettings ...
- Updated default theme colors from blue to a more neutral black and
white
# Additional changes
- Updated login docker image to node 24
---------
Co-authored-by: Ramon <[email protected]>
# Which Problems Are Solved
All http 2 based connect-es clients caused memory leaks in our testing
using the login v2 application as the host.
# How the Problems Are Solved
Switch to connect-rpc based http 1.1 transport for the login v2
# Additional Changes
Improved the otel interceptor implementation and tests.
# Additional Context
- connect-rpc is only enabled for v2 API's so we have to make sure to
not use any v1 apis in the login but this should be the case anyways.
- Discussion #10562
---------
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
Removes the Cypress integration tests and associated configuration from
the apps/login project to streamline the build process.
# How the Problems Are Solved
- Removed cypress directory, integration directory, and
cypress.config.ts from apps/login.
- Updated project.json to remove test-integration targets and
dependencies.
- Updated package.json to remove the cypress dev dependency and clean
script reference.
- Updated tsconfig.json to remove the now-redundant integration
exclusion.
- Ensured acceptance tests are preserved and correctly excluded from
build inputs.
---------
Co-authored-by: Florian Forster <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: conblem <[email protected]>
* fix: add `Scopes` to `Request` interface so that scopes can be validated on all requests
* feat: assert org from scope exists when authorizing requests
* fix: check all scopes
* comments
* check org on callback creation
* fix tests
* clarifications
* fix scope marshaling
* fix: enfore organization for authrequest in initiation
* fix: filter sessions on the accounts page by organization scope
* add integration tests
---------
Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
Instead of encoding the private key as base64 and setting it in the env
as SYSTEM_USER_PRIVATE_KEY it is sometimes easier to write it to disk
and use a file path. This is for example also possible when setting up
system users in zitadel core.
# How the Problems Are Solved
Added SYSTEM_USER_PRIVATE_KEY_FILE which is preferred over
SYSTEM_USER_PRIVATE_KEY and expects a file path to a private key in PEM
format (like SYSTEM_USER_PRIVATE_KEY but not base64 encoded). To prevent
loading the private key too often it is cached.
---------
Co-authored-by: Ramon <[email protected]>
# Which Problems Are Solved
The login v2 container has no way to verify upstream Zitadel API
connectivity before accepting traffic. The existing `/healthy` endpoint
always returns 200 regardless of whether the backend is reachable. This
means Kubernetes routes user traffic to pods that cannot actually serve
login requests, causing silent failures where the root layout swallows
gRPC errors and renders pages with default settings.
# How the Problems Are Solved
A new `/ui/v2/login/ready` endpoint creates a gRPC client using the same
auth and transport path as real requests, calls
`SettingsService.getGeneralSettings()`, and returns `200 OK` on success
or `503 Service unavailable` on failure. This verifies both network
connectivity and authentication credentials to the upstream Zitadel API.
Kubernetes supports readiness probes natively, so this endpoint can be
configured as a readiness probe to gate traffic until the login
container can connect to Zitadel. Docker Compose does not have a
readiness probe concept, so the compose healthcheck and
`healthcheck.mjs` default remain unchanged and continue using
`/healthy`.
The Docker HEALTHCHECK in the Dockerfile has been updated to use
`/ready` so that the image-level health status reflects actual upstream
connectivity. This will also allow us to simplify the Helm chart by
dropping the `wait4x` init containers that currently poll the Zitadel
API before starting the login pod, since the readiness probe makes them
redundant.
# Additional Changes
The `/ready` endpoint has been added to the OTEL HTTP instrumentation
ignore list to avoid noisy probe traces. Unit tests, a wiring spec
integration test, and an OTEL span filtering test have been added for
the new endpoint. The `# TODO: Check healthy, not ready` comment has
been removed from the Dockerfile. The `/healthy` endpoint is kept as-is
for liveness probes.
# Additional Context
The Helm chart changes to use `/ready` for readiness and startup probes
and to remove the `wait-for-zitadel` init container will be done in a
follow-up PR in the `zitadel-charts` repository.
# Which Problems Are Solved
Invalid formatting in the login codebase due to invalid prettier
configuration
# How the Problems Are Solved
Fxed .prettierignore wildcards for login source files
# Additional Changes
Removed .prettierrc file because there is already a prettier.config.mjs
file
# Additional Context
Closes#11096
# Which Problems Are Solved
This pull request resolves a 404 error that occurred at the end of the
LDAP login flow.
Previously, when a user submitted their LDAP credentials in the
apps/login application, the form initiated the session creation process
but incorrectly redirected the user to /idp/ldap/success, a route that
does not exist. In addition, the redirect failed to preserve necessary
IDP context parameters (such as requestId, organization, and linking
fingerprint info) that are required to fully complete the authentication
intent.
# How the Problems Are Solved
- Fixed redirect path: Updated `createNewSessionForLDAP` to redirect to
`/idp/ldap/process` instead of success, aligning it with the standard
IDP intent processing pipeline which dynamically resolves
`[provider]/process`.
- Preserved context parameters: Extracted `requestId`, `organization`,
`postErrorRedirectUrl`, `linkToSessionId`, and `linkFingerprint` from
the URL parameters on the LDAP page and passed them through
LDAPUsernamePasswordForm down to the server action so the ongoing auth
request is successfully tied to the user's intent.
Closes#11138
# Which Problems Are Solved
Previously, the application strictly checked the allowRegister
setting—which typically applies to local account creation—before
considering if a user without an account should be redirected to an IDP,
or whether they could access the register page. This blocked users from
registering via identity providers when local registration was disabled
but IDPs were configured.
# How the Problems Are Solved
- Removed the allowRegister dependency when deciding to redirect an
unknown user to an IDP. The logic now only checks
!allowLocalAuthentication, correctly opening up the IDP registration
flow even when local auth/registration is not allowed.
- Modified the block condition so the register page is only restricted
if allowRegister is false and there are no configured identityProviders
available.
# Which Problems Are Solved
* v2 login ui was ignoring oicd `ui_locales` making it impossible to
sync the login ui locale with the apps ui
# How the Problems Are Solved
- Read `uiLocales` from `authRequest`
- check all listed if valid in ui
- take the first valid one
- set language cookie (if a valid locale is pressent in uiLocales)
# Additional Changes
- tests
# Additional Context
---------
Co-authored-by: Florian Forster <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
# Which Problems Are Solved
The login unit tests broke due to a recent merge.
# How the Problems Are Solved
This PR fixes the broken tests.
# Additional Changes
None
# Additional Context
- requires backport to v4.x
# Which Problems Are Solved
This PR introduces several HTML attributes to the username/ID input
field to **prevent unwanted automatic capitalization and correction** on
mobile devices (iOS and Android).
Specifically, this addresses the common issue where a mobile keyboard
defaults to an uppercase letter for the first character, even when the
input (like a username or email) should be lowercase.
# How the Problems Are Solved
The following attributes have been added to the `<input>` field:
| Attribute | Value | Purpose |
| :--- | :--- | :--- |
| `autoCapitalize` | `none` | **Crucial fix:** Prevents the virtual
keyboard from capitalizing the first letter. |
| `autoCorrect` | `off` | Prevents the operating system from
auto-correcting input (e.g., changing "teh" to "the"). |
| `spellCheck` | `false` | Disables the red squiggly lines for
spell-checking. |
# Additional Changes
None.
Previously:
~Introduces a default `Content-Type` request header in the `.env` file
to increase compatibility for developing `zitadel-login` and call a
non-local Zitadel API. The `Host` header is also added as a
commented-out reference so developers will be reminded to set this one
too. This is needed for supporting calls to a Zitadel deployed behind an
AWS ALB, for example.~
https://github.com/zitadel/zitadel-charts/pull/460
# Additional Context
This attribute was already present:
| Attribute | Value | Purpose |
| :--- | :--- | :--- |
| `autoComplete` | `username` | Provides a semantic hint to password
managers to correctly suggest and fill stored usernames. |
## Rationale and References (MDN Web Docs)
| Attribute | Why we use it | Reference Link |
| :--- | :--- | :--- |
| **`autoCapitalize="none"`** | Overrides default OS sentence
capitalization behavior. | [MDN:
autocapitalize](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocapitalize)
|
| **`autoCorrect="off"`** | Essential for case-sensitive or specific
identifiers where correction must be disabled (primarily iOS). | [MDN:
autocorrect](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocorrect)
|
| **`spellCheck="false"`** | Usernames are not dictionary words, so
spell-checking is distracting/irrelevant. | [MDN:
spellcheck](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/spellcheck)
|
| **`autoComplete="username"`** | Modern standard for indicating a login
field, supporting password managers. | [MDN:
autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete)
|
## Browser Safety Concern
This change is **safe**. These attributes act as "hints" to the browser
and OS. If a particular older browser does not support one of the
attributes, it will simply ignore it and revert to its default behavior,
introducing **no errors** or breaking changes. The combination used is
considered best practice for secure and predictable input fields.
- Discussion #11124
---------
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Ramon <[email protected]>