mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
d725b9a0ed2baf949344fb78ab43fecbbf4a1cd8
4562
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d725b9a0ed |
fix(console): change username abort dialog and set email as verified #10803 #12146 (#12155)
<!-- 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 Cancelling the username change dialog in the console leads to an error toast #10803. Setting a users email as verified only worked if the email was also changed #12146. # How the Problems Are Solved Correctly destructure the abort response of the edit dialog. Use the update user endpoint instead of the set email endpoint to change the email of the user. # Additional Changes - Moved repetitive dialog code from rxjs to async await - Removed useless tests - Improved imports - Simplified phone number parsing and replaced deprecated function call # Additional Context Both changes were done in the same pr because the changes are in the same set of files. - Closes #10803 - Closes #12146 --------- Co-authored-by: Max Peintner <[email protected]> |
||
|
|
f34cc45eac |
feat(oidc): add RFC 7591 dynamic client registration (#12313)
# Which Problems Are Solved ZITADEL cannot currently be used as an OAuth 2.0 Authorization Server by clients that need to register themselves at runtime: - There is no [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) endpoint, and no `registration_endpoint` is advertised in the discovery document. - This blocks [Model Context Protocol (MCP)](https://modelcontextprotocol.io) clients (Claude Desktop, claude.ai, Cursor, the MCP SDKs), whose OAuth 2.1 authorization profile expects an Authorization Server that supports dynamic client registration. They self-register before any user context exists and only then start the authorization code + PKCE flow. # How the Problems Are Solved ## Control plane: instance security settings No feature flag and no runtime config. A `DynamicClientRegistrationSettings` message is nested under the instance `SecuritySettings` in `settings/v2`, next to `embedded_iframe` and `enable_impersonation`: | `enabled` | `allow_unauthenticated` | Behaviour | | --- | --- | --- | | `false` (default) | — | `POST /oauth/v2/register` returns `404`, `registration_endpoint` absent from discovery | | `true` | `false` (default) | Token-gated registration, client homed in the token's organization | | `true` | `true` | Open registration for MCP hosts, client homed in the instance default organization | Both values are carried on `authz.Instance` and loaded with the instance, exactly like `EnableImpersonation`, so the discovery document and the endpoint read them per request without an extra query. `allow_unauthenticated` implies `enabled` in both implementations, so a single setting closes the endpoint. Adding the two columns bumps the projection table to `projections.security_policies3`, following what the impersonation setting did for `security_policies2`. ## Authorization: a dedicated permission, not `project.app.write` - New org-scoped permission **`project.app.register_dynamic`**, granted by default to `ORG_OWNER`, `IAM_OWNER` and `IAM_ORG_MANAGER` — in both `InternalAuthZ` and `SystemAuthZ`, mirroring where `project.app.write` already lives. - New built-in role **`ORG_DYNAMIC_CLIENT_REGISTRAR`** carrying only that permission, so a service user can self-register clients without gaining write access to the organization's existing applications. - Open mode (`allow_unauthenticated = true`) requires no token and no permission: that is the MCP self-register path. - A valid token whose user lacks the permission is answered with **`403 insufficient_scope`** (RFC 6750 §3.1) rather than `401 invalid_token`, so a caller can tell "your token is bad" from "your token may not do this". `verifyAccessToken` also reports invalid tokens as permission denied, so the authorization failure is marked explicitly to keep the two apart. The check runs once, at the endpoint, right after the token is verified and before any state is created, so it also gates the auto-provisioning of the organization's DCR project. The permission itself lives in the command layer next to the others (`Commands.CheckPermissionRegisterDynamicClient`). As the OIDC endpoints are not behind the authorization interceptor, the caller identity is derived from the verified token, the way token exchange does for the impersonating actor. ## Registration itself - **Registered clients are ordinary OIDC applications.** The endpoint reuses the existing application-creation events (`NewApplicationAddedEvent` and `NewOIDCConfigAddedEvent`), so the whole token, authorization and introspection flow keeps working unchanged. They are stored in a dedicated, auto-provisioned project named `ZITADEL DCR` per organization (name lookup), so they do not pollute the `IAMProject` or other projects. - Input is validated with the existing `GetOIDCV1Compliance` rules; `private_key_jwt` and `jwks`/`jwks_uri` are rejected with `invalid_client_metadata`. - **Endpoint wiring without forking the library.** `NewServer` calls `op.RegisterServer` instead of `op.RegisterLegacyServer`, so the new route can be added through the same `op.WithSetRouter` hook as the authorize callback. `op.RegisterLegacyServer` appends a middleware after the caller options, which chi forbids once a route has been registered; `op.NewIssuerInterceptor` reproduces the issuer middleware it would otherwise add. The `/oauth/v2` routing prefix is unchanged. # Additional Changes - **`application_type` inference for custom-scheme redirects** (last commit, isolated). `application_type` is an OpenID Connect member; RFC 7591 does not define it, so clients that only implement RFC 7591 omit it. Native MCP hosts doing so register custom-scheme redirect URIs, which ZITADEL reserves for native applications, and the OIDC default of `web` rejected them with `invalid_redirect_uri`. Inference is limited to custom schemes and to an absent `application_type`, so every request accepted today keeps its current application type and auth method — an `http` loopback redirect still yields a web application. Happy to drop this commit if you would rather keep it out of this PR. - **Docs**: a Dynamic Client Registration integration guide, a `registration_endpoint` section on the OpenID Connect endpoints page, and the new role in the administrators table (plus its description in the Console translations). # Additional Context - Part of #9810. This is **Phase 1 (DCR)**; #12315 adds RFC 7592 and #12316 adds CIMD, both stacked on this branch and rebased once this lands. - Implements the control-plane and authorization model requested in the review of this PR, itself a refinement of the direction in https://github.com/zitadel/zitadel/issues/9810#issuecomment-4891176997. - **Out of scope (deliberately)**: RFC 7592, CIMD, persisting free-form metadata (`logo_uri`, `contacts`, and similar), and `private_key_jwt` / `jwks_uri` (rejected cleanly). No new event types. - Follow-ups explicitly left out, per the review: DCR client lifecycle and richer audit, consent and custom scopes, JWT / `aud` (RFC 8707), and RFC 8414 AS metadata (currently absent from both ZITADEL and `zitadel/oidc`). |
||
|
|
adb2974662 |
fix: add the dropped idpTemplate argument (#12511)
# Which Problems Are Solved This PR fixes a Go compilation error in the Login V1 caused by a previously dropped `idpTemplate` argument when calling `registerExternalUser`. # How the Problems Are Solved - Pass `idpTemplate` into `registerExternalUser` from `handleExternalNotFoundOptionCheck` to match the function signature. # Additional Changes N/A # Additional Context introduced in https://github.com/zitadel/zitadel/commit/a17af0fe934ff94fcbf6c38d56aee308e1694962#diff-ec5370dd3f47f190c01e8dee4fd027b334a523cf31e5557422dd14c6e6c90339R859 |
||
|
|
10a66b786b |
fix(login): honour login_hint and skip the auto-submit loop in OIDC flow (#12431)
<!-- 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 An OIDC authorize request that carries a `login_hint` (and no `prompt`) is meant to land the user on a **prefilled** login screen and let them proceed straight to authentication. Two problems prevented that: **1. The hint was dropped when an unrelated session existed.** When the browser held a `sessions` cookie for a *different* account: - The user was redirected to the **account picker** (`/accounts`) instead of the prefilled login-name screen. - The picker listed the already-logged-in, unrelated account(s) — never the user identified by `login_hint`. - The `login_hint` was **silently dropped** (the `/accounts` URL carries no hint), so the typed email was lost and the flow dead-ended on the wrong accounts. In a clean session (no `sessions` cookie) the same request prefilled correctly, which made the bug depend on unrelated pre-existing sessions. **2. The prefilled path was jarring and looped on "back".** Even when the hint *was* honoured, the flow redirected to `/loginname?loginName=…&submit=true`. That page renders the username screen and then **client-side auto-submits** it, so the user sees the username flash on screen, watches "Continue" click itself, and only then lands on the password step. Worse, `/loginname?submit=true` stays in browser history, so pressing **Back** from the password screen re-triggers the auto-submit and bounces the user straight forward again — an inescapable loop with no way to edit the username. # How the Problems Are Solved **Honour the hint (problem 1).** In `handleOIDCFlowInitiation`, the default branch (no `prompt`) already calls `findValidSession`, which filters the browser's sessions by `login_hint` and treats the hint as *selecting the user*. When the hint matches none of the sessions it returns `undefined`, and the remaining `eligibleSessions` (filtered by organization only) are therefore never the hinted user. The fallback previously chose between the loginname screen and the account picker based solely on `eligibleSessions.length === 0`, so a present-but-unrelated session sent the user to `gotoAccounts`, discarding the email. The loginname branch is now also gated on `authRequest.loginHint`, mirroring the existing empty-eligible-sessions branch. **Skip the flash and the loop (problem 2).** Instead of bouncing through `/loginname?submit=true` and relying on client-side auto-submit, the hint is now resolved **server-side** via `sendLoginname` (the same approach the `Prompt.LOGIN` branch already used) and the user is redirected **straight to the next step** (e.g. `/password`), skipping the `/loginname` screen entirely. This is factored into a shared `resolveLoginHint` helper and applied to all `login_hint` branches (default-prompt, no-session, and the existing `Prompt.LOGIN` path, now deduplicated). When the hint can't be resolved (unknown/ambiguous user, transient error), the flow falls back to a prefilled `/loginname` **without** `submit=true` — a clean screen where the user clicks once — so there is no auto-submit and no back-button loop in any path. ```ts if (!selectedSession || !selectedSession.id) { // login_hint matches no session: resolve it straight to the next step. if (authRequest.loginHint) { const hintResponse = await resolveLoginHint({ request, requestId, loginHint: authRequest.loginHint, organization }); if (hintResponse) { return hintResponse; } } // Prefill loginname (not the account picker) for an unresolved hint or when no session is eligible. if (authRequest.loginHint || eligibleSessions.length === 0) { return gotoLoginname({ request, requestId, loginHint: authRequest.loginHint, organization, orgDomain }); } return gotoAccounts({ request, requestId, organization, orgDomain }); } ``` Resulting behaviour: - `login_hint` set, a session **matches** it → silent auth (**unchanged**). - `login_hint` set, **no** session matches, hint **resolves** → straight to the next step (e.g. `/password`), no `/loginname` flash (**was**: account picker with the hint dropped / a jarring auto-submit screen). - `login_hint` set, hint **can't** be resolved → prefilled `/loginname`, no auto-submit, Back stays editable. - **No** `login_hint` → account picker / loginname exactly as before (**unchanged**). # Additional Changes - `resolveLoginHint` helper extracted in `flow-initiation.ts`; the `Prompt.LOGIN` branch now reuses it instead of duplicating the `sendLoginname` call. - `submit=true` is no longer emitted for the login-name flow, so the client-side auto-submit in `username-form.tsx` is no longer reached from OIDC initiation. - Tests in `flow-initiation.test.ts`: each `login_hint` branch (org-scope-filtered, unrelated-session, no-session) is covered twice — hint resolves → straight to `/password`; hint unresolved → prefilled `/loginname` with **no** `submit=true`. # Additional Context - The org-only `eligibleSessions` check was introduced in #12346, which correctly handled the *empty* case but did not account for a present-but-unmatched `login_hint`; this PR extends that gate. - Per OIDC, `login_hint` is advisory prefill; a client that wants the chooser would send `prompt=select_account`. Resolving the hint server-side also matches how the `Prompt.LOGIN` branch already behaved. --------- Co-authored-by: Max Peintner <[email protected]> Co-authored-by: Max Peintner <[email protected]> |
||
|
|
a17af0fe93 |
Merge commit from fork
* fix(login): prevent external-IDP account pre-hijack in Login V1 (GHSA-738m-7888-jfv8) The Login V1 endpoint POST /ui/login/externaluser/option (handleExternalNotFoundOptionCheck) built the new external user's identity and "verified" flags directly from raw POST fields, without checking the server-side authReq.LinkingUsers state that a genuine, protocol-verified IDP callback populates. An unauthenticated attacker could forge that POST to create a real account bound to an arbitrary (IDPConfigID, ExternalUserID) - enabling account pre-hijack of a third party whose public external identifier (e.g. a GitHub numeric user id) was used, so the victim's own future "Sign in with X" logs into the attacker's pre-created account. Primary fix: - Reject the request when authReq.LinkingUsers is empty, mirroring the guarded sibling autoCreateExternalUser. - Source IDPConfigID/ExternalUserID and the email/phone "verified" flags from the trusted linkingUser (the real callback data) instead of the form; the verified flag only survives when the editable value still matches the IDP's. Profile fields (name, nickname, language, edited email/phone) still come from the form, which is the page's legitimate purpose. Secondary hardening: - Treat the OAuth error/error_description callback params as untrusted so a forged GET .../externalidp/callback?error=... can no longer trigger the local-auth fallback (RequestLocalAuth) and undermine the IDP-first login priority. Genuine code-exchange failures keep their existing handling. Tests: - Unit test for mapExternalNotFoundOptionFormDataToLoginUser proving forged identity/verified fields are ignored in favor of the trusted callback data. - A //go:build integration end-to-end reproduction driving the forged registration over raw HTTP and asserting the account is not created. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): handle IDP error callback for logging, keep local-auth fallback Addresses review feedback (livio-a) on GHSA-738m-7888-jfv8: the OAuth error callback is not a security boundary. Blocking only the error= path added no protection (a garbage code triggers the same fallback), the downgrade only affects the attacker's own auth request, and a genuine IDP error on the callback should still fall back to local auth. - handleExternalLoginCallback: on an IDP error response, log it with context and route through externalAuthCallbackFailed so local-auth fallback still happens when the login policy allows it (previous behavior), instead of rendering a plain login error. The displayed message reuses the existing Errors.User.ExternalIDP.LoginFailedSwitchLocal via WrapIdPError. - Drop the now-unused Errors.ExternalIDP.AuthenticationFailed translation (would otherwise need all-language entries) and regenerate the login statik blob. - Reword the externalIDPCallbackData Error/ErrorDescription comment: logging-only, not a security decision. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: Livio Spring <[email protected]> |
||
|
|
fe935d91b8 | Merge commit from fork | ||
|
|
6fa5c0e662 |
Merge commit from fork
* feat(login): add shared enrollment authorization guard Introduce assertSessionAuthorizedForEnrollment, a single gate that decides whether a session may enroll a new authenticator: it requires a verified primary factor (password, passkey or IDP) on a non-expired session, or, for onboarding, a user with no auth methods plus a prior user-verification check. This mirrors the gate previously inlined only in registerPasskeyLink, which is refactored to reuse the shared helper. It is the foundation for gating the remaining credential-enrollment entry points (GHSA-45f2-5q3r-xgg6). Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before WebAuthn/U2F enrollment addU2F and verifyU2F registered a credential for any session that resolved to a user id, including a bare identify-only session created by merely submitting a login name. An unauthenticated attacker who knew a victim's login name could therefore attach an attacker-controlled authenticator to the victim's account and then complete a login as the victim. Both actions now run assertSessionAuthorizedForEnrollment before registering or verifying, so an unauthenticated session is rejected and no credential is persisted. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before TOTP/OTP enrollment verifyTOTP and the otp/<method>/set page activated a TOTP/OTP factor for any session with a user id, and the u2f/set page rendered the registration form without an authorization check. An identify-only session could thus enroll a second factor on a victim's account. Gate all of them with assertSessionAuthorizedForEnrollment. The setup pages now render the unknown-context/error state instead of enrolling when the session is not authorized. Also guard the method value in the OTP set page, since the added guard branch removes the control-flow narrowing the previous throw provided. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): require authentication before legacy (V1) passwordless enrollment The V1 passwordless registration handlers began and verified passwordless setup for any auth request that carried a user id, without checking that a first factor had been verified. An unauthenticated attacker could submit a victim's login name and then POST directly to the passwordless prompt/init endpoints to plant an attacker-controlled passkey, then log in as the victim. Add passwordlessSetupAllowed / CheckPasswordlessSetupAllowed, which permit enrollment only for a legitimate onboarding user (PasswordlessInitRequired) or an auth request whose first factor is already verified, and call it before BeginPasswordlessSetup and VerifyPasswordlessSetup. The verify path is also bound to authReq.UserID instead of the attacker-controllable form field, so a credential cannot be redirected onto a different account. The init-code (mail/onboarding) path is unchanged. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(login): remove never-active legacy (V1) authReq passwordless setup path Supersedes the runtime gate added in the previous commit. Maintainer review (Livio, the original author) and investigation confirmed the auth-request based passwordless *setup* path in the legacy login was a never-active leftover: - The passwordless prompt page (passwordless_prompt.html) has no submit button and no script; it only tells the user to complete setup via the emailed link (the PasswordlessButtonText/NextButtonText/SkipButtonText i18n keys are unused). - Legitimate onboarding for a PasswordlessInitRequired user goes through the emailed init-code flow (authReq == nil, BeginPasswordlessInitCodeSetup / VerifyPasswordlessInitCodeSetup), which is untouched. - The authReq branch (handlePasswordlessPrompt -> BeginPasswordlessSetup / VerifyPasswordlessSetup) was reachable only by a crafted direct POST — the GHSA-45f2-5q3r-xgg6 attack. Rather than gate the dead path, remove it entirely, eliminating the attack surface: - delete handlePasswordlessPrompt and the POST /login/passwordless/prompt route (keep renderPasswordlessPrompt: the informational "use your emailed link" page for PasswordlessRegistrationPromptStep), - make renderPasswordlessRegistration / checkPasswordlessRegistration use only the init-code path, - drop the now-unused AuthRequestRepo.BeginPasswordlessSetup / VerifyPasswordlessSetup (and the interface decls) and the passwordlessPromptUrl template helper; the underlying HumanAddPasswordlessSetup / HumanHumanPasswordlessSetup commands remain (used by the auth-API self-service and the v2 passkey APIs). A crafted POST to /login/passwordless/init carrying an authRequestID but no valid code now falls through to the init-code path and fails. GHSA-45f2-5q3r-xgg6 Co-Authored-By: Claude Opus 4.8 <[email protected]> * check for userverified true as well, cleanup return message --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: Max Peintner <[email protected]> |
||
|
|
ffd9b39bdb |
feat: handle Zitadel provider IdP login via login v1 (#12469)
# Which Problems Are Solved This PR adds support for using a ZITADEL IdP provider in the login v1 UI flow and conditionally grants a support-user instance membership based on a ZITADEL-specific project-roles token claim. # How the Problems Are Solved - Introduced ZITADEL IdP handling in the login v1 handler, including extracting the `urn:zitadel:iam:org:project:roles` claim and granting `IAM_OWNER_VIEWER` instance membership when configured orgs match. - Added a new `AddInstanceMemberFromLogin` to assign an instance membership during the login v1 ZITADEL-IdP flow without permission checks (as the authorization is established based on the project roles in the token claim) - Added unit tests # Additional Changes N/A # Additional Context - Closes https://github.com/zitadel/zitadel/issues/12446 |
||
|
|
318acd4bc3 |
perf(query): speed up ListUsers login name equality filters (#12460)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary `ListUsers` with `LoginNameQuery` + equals/equals-ignore-case was very slow on large orgs (e.g. ~330k users) because the generated SQL filtered the `projections.login_names3` **view** on the computed `login_name_lower` expression and correlated that subquery on `users14.id`. Postgres nested-looped every user and never used `login_names3_users_search (instance_id, user_name_lower)`. This change rewrites the **query planner** for that hot path: when an equals/equals-ignore-case login-name filter is present (and not under `OR`/`NOT`), the user list query **INNER JOINs** an indexed matches subquery instead of filtering via the view expression. The matches SQL mirrors `user_by_login_name.sql` (`user_name_lower` / domain paths + `preferred` / `is_primary`). Non-equals methods and OR combinations keep the previous view-based filter so semantics stay unchanged. Also adds a k6 use case that mirrors login v2 discovery (`loginNameQuery` EQUALS_IGNORE_CASE + `organizationIdQuery`, `limit: 2`): ```bash cd benchmark make users_by_login_name USER_AMOUNT=100000 VUS=10 DURATION=60s ``` ## Approach 1. `NewLoginNameSearchQuery` for equals / equals-ignore-case returns a marker `loginNameEqualsFilter` (other methods unchanged). 2. `prepareUsersQuery` extracts that marker when safe, then: - builds the usual `sq.SelectBuilder` **without** the login-name view predicate - adds `JoinClause` to `user_login_name_matches(.sql)` / `_case_sensitive.sql` as `login_name_matches` - keeps metadata JOIN/`DISTINCT` only when metadata filters are present (same as before) 3. Embedded SQL files under `internal/query/` for the matches subquery. Local smoke against ~330k synthetic users: baseline ~1392ms → rewritten path ~0.4ms for a single equals-ignore-case lookup. ## Test plan - [x] `go test ./internal/query/ -run 'TestLoginName|TestUsers|TestUserByLoginName'` (after generate-stubs) - [x] Existing `user_test` expected SQL updated (no always-on metadata join; login-name equals uses JOIN) - [ ] Run k6 before/after on a large `USER_AMOUNT` (e.g. 50k–100k+) and compare `list_users_duration` p50/p95/p99 - [x] Manual login v2 username discovery against a large org - [x] Confirm OR / NOT / CONTAINS login-name queries still return expected results <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-252883bd-48d9-492e-b619-5ccfa93cf9c3"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-252883bd-48d9-492e-b619-5ccfa93cf9c3"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Silvan <[email protected]> |
||
|
|
87538f29e1 |
docs: prefix legacy V1 API paths in generated OpenAPI (#12491)
# Which Problems Are Solved
- Generated V1 API reference docs are missing their base path prefixes
(e.g. `/admin/v1`, `/auth/v1`, `/management/v1`, `/system/v1`).
- Endpoint pages show routes like `/healthz` instead of
`/admin/v1/healthz`.
This happens because V1 protos still define `base_path` via legacy
OpenAPI v2 swagger annotations, while docs generation uses
`protoc-gen-connect-openapi`, which ignores those annotations and emits
the bare `google.api.http` paths.
# How the Problems Are Solved
- After OpenAPI generation in
`apps/docs/scripts/generate-proto-docs.mjs`, apply a small hardcoded map
of the four legacy V1 services and prepend the correct prefix to each
path in the generated specs.
- The rewrite is idempotent (`startsWith(prefix)`), so already-prefixed
paths are left alone.
- Because the prefixes are written into the generated OpenAPI files,
Fumadocs MDX generation and runtime page rendering stay in sync.
# Additional Changes
- None.
# Additional Context
- V1 APIs are legacy and frozen; hardcoding the four known prefixes is
simpler and more reliable than dynamically scraping `.proto` files.
- Changing the protos or dual-generating with `openapiv2` would be much
more invasive for a docs-only issue.
- Verified locally for Admin, Auth, Management, and System pages (e.g.
`/admin/v1/healthz`, `/management/v1/users/{id}`).
---------
Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
|
||
|
|
ca6595f8c5 |
chore: google.golang.org/grpc deps update (#12484)
# Which Problems Are Solved google.golang.org/grpc < 1.82.1 had a high severity vuln reported here: https://github.com/zitadel/zitadel/security/dependabot/784 . Needs to be addressed # How the Problems Are Solved `go get -u google.golang.org/grpc` # Additional Context - Closes https://github.com/zitadel/zitadel/security/dependabot/784 |
||
|
|
bb4f546b7c |
docs: add video walkthrough link to quickstart guide (#12479)
Link the YouTube walkthrough at the end of the quickstart page for users who prefer watching over reading. Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
62e56e085c | docs: fixed broken docs references (#12465) | ||
|
|
d78ed6b5f2 |
fix: improve random string generation (#12266)
# Which Problems Are Solved `GenerateRandomString` used for generating codes incl. OTP was incorrectly ignoring the last rune of the possible set. # How the Problems Are Solved Refactored the function to get the full randomness. # Additional Changes None # Additional Context Thanks @AyushParkara for pointing this out. |
||
|
|
3e94047da7 |
fix(login): redirect to external IdP after domain discovery regardless of registration policy (#12369)
Closes #12021 Closes #12023 # Which Problems Are Solved When a user entered an email on the login page and domain discovery resolved the correct organization, the login flow failed to redirect to the configured external IdP. Instead it returned a "user not found" error or showed the registration page. This happened because the "user not found" decision tree in `sendLoginname` only attempted an IdP redirect when `allowLocalAuthentication` was disabled. If local auth was enabled but `allowRegister` was `false`, the IdP check was skipped entirely. # How the Problems Are Solved Combined the IdP redirect logic into a single block that triggers when either local authentication is disabled **or** domain discovery resolved an organization. The registration policy (`allowRegister`) now only affects local account creation and no longer gates external IdP authentication. --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Livio Spring <[email protected]> |
||
|
|
2b9b9856b4 |
docs: remove changelog link from sidebar (#12459)
The changelog page on zitadel.com was removed for being unmaintained, leaving the docs sidebar link 404ing. Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
437a0802bd |
docs: document initial admin password requirements in compose guide (#12452)
# Which Problems Are Solved - The **Harden secrets** section of the Docker Compose guide lists commands for the masterkey and the two database passwords, but never mentions the **initial admin user password**. - Setting `ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD` to a value that does not meet the default password complexity policy fails the initial setup with a `_password_complexity_model` error, and the requirements were not documented anywhere near the other secrets. # How the Problems Are Solved - Adds a short note directly below the secret-generation commands documenting the initial admin password: the default (`Password1!` / `[email protected]`), how to override it via `ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD`, and the default complexity requirements (min 8 chars, upper + lower + number + symbol). - Links to the [Password Complexity](/guides/manage/console/default-settings#password-complexity) policy docs and notes that the variable only applies during initial setup. # Additional Changes None. # Additional Context - Reported by a self-hosting user (TrueNAS + Ansible) who hit repeated setup errors because the initial admin password complexity requirement was not documented alongside the masterkey/DB password commands. |
||
|
|
b91221143b |
feat: extend start/retrieve idp intent for Zitadel provider (#12424)
# Which Problems Are Solved This PR extends `StartIdentityProviderIntent` and `RetrieveIdentityProviderIntent` for the new Zitadel provider. # How the Problems Are Solved - Added `domain.IDPTypeZitadel` cases to both the instance and org switches in `NewAllIDPWriteModel` to return Zitadel provider when `GetProvider` is called - Added Zitadel provider to RetrieveIdentityProviderIntent's provider type-switch - Added new `zitadel.go` provider, an OIDC wrapper with forced PKCE - Added unit/integration tests # Additional Changes # Additional Context - Closes https://github.com/zitadel/zitadel/issues/12050 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
53d7920349 |
fix(console): display minimum length in password complexity message (#12419)
# Which Problems Are Solved
During user registration and password changes, the console's password
complexity hint displayed the raw `{{value}}` placeholder instead of the
configured minimum length — e.g. `Must be at least {{value}} characters
long (0/8)`. The bug is language-independent (confirmed in English and
Dutch).
# How the Problems Are Solved
The `password-complexity-view` template passed `policy.minLength` — a
proto `uint64` that deserializes to a JavaScript `bigint` — to the
ngx-translate pipe. ngx-translate v17's `formatValue` returns
`undefined` for a `bigint`, so the `{{value}}` token was never
substituted. The template now passes the component's existing
`minLength` getter (`Number(policy.minLength)`), so the value
interpolates correctly.
# Additional Changes
None
# Additional Context
- Closes #12390
- Reported on ZITADEL Cloud v4.15.3
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Ramon <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
|
||
|
|
56f4798ed3 |
Merge commit from fork
* add mfa check to session validity check * change session check to check for already set up mfa methods * lint * improve naming * test changes * cleanup continueWithSession fcn |
||
|
|
baf6ed501b |
Merge commit from fork
* fix(actions): prevent disk access via require * add test to ensure native modules are still possible |
||
|
|
94ac4237a9 |
docs: add user self-deletion guide (#12458)
## Summary - Add a new guide documenting how to let users delete their own account via the User v2 API's `DeleteUser` endpoint, gated by the `user.self.delete` permission (granted via `ORG_USER_SELF_MANAGER`) - Link the new guide from the self-service concepts page and sidebar - Add the `ORG_USER_SELF_MANAGER` role to the administrators reference table ## Test plan - [x] Verified the new and updated pages render correctly in local dev (`pnpm dev`) replaces #12455 and #12457 (branch got renamed from `docs/human-pat-clarification`, which auto-closed #12457). Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
75bc058bce |
feat(eventstore): autovacuum tuning for events2 table (#12449)
# Which Problems Are Solved
In Zitadel's append-only event-sourced architecture, the
`eventstore.events2` table grows indefinitely. PostgreSQL's default
autovacuum uses a percentage-based scale factor, so as the table grows,
the number of changed rows required to trigger a `VACUUM` or `ANALYZE`
drifts towards infinity. Without regular vacuums, the table's Visibility
Map becomes stale, disabling fast Index-Only Scans and forcing expensive
heap reads. Without regular analyzes, query planner statistics become
stale, leading to suboptimal execution plans.
This causes eventstore operations to progressively degrade as `events2`
grows, even without CPU, memory, or I/O saturation. A manual `VACUUM
ANALYZE` immediately restores performance, confirming the root cause.
- If `Eventstore.Autovacuum` is left at its default, `events2` keeps
using PostgreSQL's default, percentage-based autovacuum/autoanalyze
scale factors, which become impractically infrequent on large tables.
- There was previously no supported way to apply static, table-level
autovacuum tuning to `events2` through Zitadel's own configuration/setup
process.
# How the Problems Are Solved
- Added an `Eventstore.Autovacuum` runtime configuration block to
`cmd/defaults.yaml` (disabled by default):
```yaml
Eventstore:
Autovacuum:
Enabled: false # ZITADEL_EVENTSTORE_AUTOVACUUM_ENABLED
VacuumThreshold: 50000 # ZITADEL_EVENTSTORE_AUTOVACUUM_VACUUMTHRESHOLD
AnalyzeThreshold: 50000 # ZITADEL_EVENTSTORE_AUTOVACUUM_ANALYZETHRESHOLD
```
- Added a repeatable `zitadel setup` migration step
(`cmd/setup/eventstore_autovacuum.go`) that:
- When `Enabled: true`, disables the percentage-based
`autovacuum_vacuum_scale_factor`, `autovacuum_analyze_scale_factor`, and
`autovacuum_vacuum_insert_scale_factor` on `eventstore.events2`, and
applies static thresholds (`autovacuum_vacuum_insert_threshold`,
`autovacuum_vacuum_threshold`, `autovacuum_analyze_threshold`) from the
config instead.
- When `Enabled: false`, resets those storage parameters on
`eventstore.events2` back to the cluster defaults.
- Implements `Repeatable.Check()` so the step only re-runs when the
configuration actually changed since the last `zitadel setup` run.
- Added documentation in the new "Performance tuning" page.
# Additional Changes
- Move the projection documentation into performance tuning page
- Expand and update the projection documentation to the latest state in
zitadel. (contained some stale information)
# Additional Context
Several other issues describe read-performance symptoms consistent with
this same root cause (stale `events2` visibility map / planner
statistics at scale, without resource saturation). Since this PR
addresses the shared root cause:
- Closes #12448
- Closes #10754
- Closes #10260
- Closes #8585
- Closes #9239
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HoKvEY7niCVgLCz7CajBwW)_
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Silvan <[email protected]>
|
||
|
|
6bd46c5fb6 |
feat(login): idp userAction oneof (#12433)
Closes #11369 # Which Problems Are Solved During SSO login with an external IdP, Actions v2 lets you manipulate the `RetrieveIdentityProviderIntent` response to shape the user that ZITADEL creates or updates. There was an inconsistency between the two flows: - **First login (user does not exist):** the response carried an `addHumanUser` object (mirroring the deprecated `AddHumanUser` API), which **does** allow setting user `metadata`. - **Subsequent logins (user exists):** the response carried an `updateHumanUser` object (mirroring the deprecated `UpdateHumanUser` API), which does **not** support metadata. As a result, actions could set metadata when creating a user but not when updating one. Customers doing SSO attribute mapping had to make a separate `SetUserMetadata` call on every subsequent login — extra latency and a non-atomic update. The proto/backend already gained a non-deprecated `user_action` oneof (`create_user` → `CreateUserRequest`, `update_user` → `UpdateUserRequest`, both supporting metadata), but the login app was still reading the deprecated flat fields, so the new capability was unreachable from the frontend. # How the Problems Are Solved Migrate the login app's IDP intent handler to consume the new `user_action` oneof, with a fallback to the deprecated fields so older API responses keep working during the transition. - **`zitadel.ts`** — added `createUser` / `updateUser` client wrappers calling the non-deprecated `UserService.CreateUser` / `UserService.UpdateUser` endpoints. - **`idp-intent.ts`** — added three helpers, each preferring `user_action` and falling back to `add_human_user` / `update_human_user`: - `resolveCreateUser` — flat read view for org resolution, required-field checks, and registration-form pre-fill. - `buildCreateUserRequest` — passes the action's `CreateUserRequest` through and injects the resolved `organizationId`; maps the deprecated flat payload into the nested shape on fallback. - `buildUpdateUserRequest` — builds an `UpdateUserRequest` **including metadata** (the fix); deliberately syncs only profile/email/phone/metadata (not username) to preserve existing auto-update behavior and avoid invalidating sessions. - Rewired all handlers (`handleUserExists`, `handleAutoLinking`, `handleAutoCreation`, `handleManualCreation`, `resolveOrganizationForUser`) to use these, and switched auto-create to read `CreateUserResponse.id`. - **Tests** — updated mocks/assertions to the new request shapes and added two cases exercising the `user_action` oneof with metadata (create + update). 817/817 login unit tests pass; no new type errors. # Additional Changes Updated the Actions v2 guide `guides/integrate/actions/testing-response-manipulation.mdx` (the unreleased/`latest` docs) to reflect the new response shape: - Go handler example now manipulates `resp.GetCreateUser()` / `resp.GetUpdateUser()` and appends `user.Metadata`, demonstrating metadata on both flows. - Both JSON payloads switched from `addHumanUser` to the nested `createUser` shape (`human.profile`, `human.email`, `human.idpLinks`, top-level `metadata`). - Added a Callout explaining first-login → `createUser` vs. existing-user → `updateUser`, that both support metadata, and that `addHumanUser`/`updateHumanUser` are deprecated. - Updated the claim-mapping debugging section to the new `createUser.human.profile.givenName` path. Versioned snapshots (`v4.12`/`v4.13`/`v4.14`) were intentionally left unchanged, as they document releases where the old API was correct. --------- Co-authored-by: gayathri <[email protected]> |
||
|
|
8395d4326e |
docs: add v3 to v4 upgrade and Login V2 adoption guides (#12443)
<!-- 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 - Operators upgrading from ZITADEL v3 to v4 lacked a clear, docs-backed upgrade path covering web keys (A-10017), Login V1 vs Login V2 expectations, and post-upgrade options. - There was no standalone guide for adopting Login V2 after already running on v4. - Related ops/docs pages did not consistently point readers to the upgrade and Login V2 adoption material. - Kubernetes/Helm operators upgrading to a chart that ships ZITADEL v4 had no ops-page section linking the advisory, upgrade guide, optional Login V2 deferral (`login.enabled: false`), and chart README upgrade notes. # How the Problems Are Solved - Adds technical advisory **A-10017** documenting OIDC web key staging requirements before upgrading to v4. - Adds an **upgrade-v3-to-v4** guide with the recommended upgrade path (Login V1 remains supported; Login V2 is optional at upgrade time). - Adds a **v4-only adopt-login-v2** guide for teams adopting Login V2 later, with steps and caveats separate from the version upgrade. - Adds an **Upgrading to ZITADEL v4** subsection under `self-hosting/deploy/kubernetes/operations.mdx` that links to A-10017, the upgrade guide, Adopt Login V2, notes `login.enabled: false` when not adopting Login V2 yet, and points to the [Helm chart README](https://github.com/zitadel/zitadel-charts/blob/main/charts/zitadel/README.md) for chart-specific breaking changes. # Additional Changes - Registers new docs in the sidebar and wires Related / cross-links from upgrade and ops pages so the advisory and guides are discoverable together. - Aligns language so upgrade vs. Login V2 adoption are clearly separated concerns. - Light cross-link updates on troubleshooting / updating-scaling / login-client pages where related. # Additional Context - Related: A-10017 (web keys advisory) - Docs paths introduced/updated: - `apps/docs/content/support/advisory/a10017.mdx` - `apps/docs/content/self-hosting/manage/upgrade-v3-to-v4.mdx` - `apps/docs/content/self-hosting/manage/adopt-login-v2.mdx` - `apps/docs/content/self-hosting/deploy/kubernetes/operations.mdx` (new "Upgrading to ZITADEL v4" section) - Sidebar and Related / cross-links - Companion Helm chart README PR: https://github.com/zitadel/zitadel-charts/pull/608 --------- Co-authored-by: Cursor <[email protected]> |
||
|
|
93209bd348 |
chore(deps): update benchmark dependencies (#12442)
# Which Problems Are Solved Outdated deps on the benchmark typscript modules. # How the Problems Are Solved Upgrade all deps # Additional Changes - Add xk6 a prerequisite for running benchmarks - Update the moduleResolution setting in tsconfig to a non-deprecated value - Some automatic code formatting --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
1e09b4fc19 |
feat(login): hu translations + I18n consistency v2 (#12447)
# 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]> |
||
|
|
305579dd07 |
docs: update V2 end-of-support copy in roadmap (#12435)
## Summary - V2 has already reached end of support; the roadmap page previously implied end-of-support timelines for V2 and V3 were both still forthcoming. - Updated copy to state V2's end-of-support status plainly and note that V3's timeline and migration guidance will be published soon. ## Test plan - [x] Verified the updated sentence renders correctly in `apps/docs/content/product/roadmap.mdx` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Elina Sokolovska <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> |
||
|
|
11e71e969a |
docs(deps): upgrade image versions in env examples (#12441)
# Which Problems Are Solved Image versions in the example env, used in the docker quickstart, were outdated. # How the Problems Are Solved Update all image versions. |
||
|
|
f0a999bccd |
docs: clarify human PAT limitation and link issue #10915 (#12412)
## Which Problems Are Solved There is currently a knowledge gap in the documentation where users are unclear on if or why human accounts cannot have Personal Access Tokens (PATs), which has led to confusion in community channels like Discord and GitHub. ## How the Problems Are Solved Clarified that human PATs are currently not supported in the Personal Access Token documentation and added a direct link to GitHub Issue #10915 so users can track and upvote this capability. ## Additional Changes None ## Additional Context None --------- Signed-off-by: Rajat Singh <[email protected]> Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
6eafca2872 |
feat: update GetActiveIdentityProviders to return Zitadel in the IdP list (#12422)
# Which Problems Are Solved This PR extends the Settings Login API (v2 and v2beta) to include ZITADEL IdP in the list of IdPs returned by `GetActiveIdentityProviders` # How the Problems Are Solved - Added `IDENTITY_PROVIDER_TYPE_ZITADEL` to Settings IdentityProviderType enums (v2 and v2beta) and updated domain display name handling. - Updated Settings gRPC converters (v2 and v2beta) to map `domain.IDPTypeZitadel` to the new proto enum value - Added unit/integration tests # Additional Changes Correctly map `domain.IDPTypeApple` to `IdentityProviderType_IDENTITY_PROVIDER_TYPE_APPLE` in v2beta instead of unspecified. # Additional Context - Closes https://github.com/zitadel/zitadel/issues/12401 --------- Co-authored-by: Livio Spring <[email protected]> |
||
|
|
051506b5bb |
fix(login): prevent crash on a stale session cookie (#12423)
<!-- 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]> |
||
|
|
aa76c3be27 |
fix(login): keep submit button disabled/loading during password set r… (#12429)
<!-- 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]> |
||
|
|
b429ec240a |
docs: remove GitHub discussion link from TOTP issuer callout (#12428)
## Which Problems Are Solved The callout added in #12335 included a link to a GitHub discussion which will become outdated and difficult to maintain. ## How the Problems Are Solved Removes the discussion link from the ZITADEL Cloud bullet point in the TOTP issuer callout, keeping the rest of the callout intact. Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
6bcdb27e17 |
docs: document TOTP issuer name configuration for self-hosted and cloud instances (#12335)
# Which Problems Are Solved - No documentation explained that the TOTP issuer name defaults to "ZITADEL" and is not derived from the domain - No documentation clarified how to change it on self-hosted (env var only, Helm values don't work) - No documentation communicated that it is not configurable on ZITADEL Cloud # How the Problems Are Solved - Adds a callout in the MFA section of the default settings page covering both self-hosted (ZITADEL_SYSTEMDEFAULTS_MULTIFACTORS_OTP_ISSUER) and cloud (not configurable, discussion link) # Additional Changes None # Additional Context - https://github.com/zitadel/zitadel/discussions/5453 Signed-off-by: Rajat Singh <[email protected]> Co-authored-by: Rajat Singh <[email protected]> |
||
|
|
5bdfd96be5 |
feat: extend DeleteProvider to include Zitadel IdP (#12396)
# Which Problems Are Solved This PR extends `DeleteProvider` to include Zitadel provider enabling the deletion of Zitadel IdP. # How the Problems Are Solved - Extend org/instance IDP remove write models to include `ZitadelIDPAddedEvent` in event appends and queries. - Extend command-side IDP reduction/type handling for Zitadel IDP add events. - Add management/admin integration tests for deleting Zitadel providers - Add error translation key in all language locales for org-level “IDP config not existing”. # Additional Changes N/A # Additional Context. Closes https://github.com/zitadel/zitadel/issues/12397 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Livio Spring <[email protected]> |
||
|
|
a97999e5ca |
docs: update roadmap page content (#12402)
## Summary - Update the description frontmatter and reword "next generation" to "next iteration" throughout - Add a "What this means for existing customers" subsection after the disclaimer, before the Strategic Roadmap section - Convert `### **Strategic Investments**` / `### **Customer Outcomes**` subheadings to bold text so they no longer appear in the TOC - Rename the closing section to "Migration and Adoption" with updated content ## Test plan - [x] Diffed against source content to confirm all requested sections match - [x] Verified no remaining "next generation" occurrences --------- Co-authored-by: Elina Sokolovska <[email protected]> Co-authored-by: Florian Forster <[email protected]> |
||
|
|
77169181a3 |
feat: get/list Zitadel IdP (#12394)
# Which Problems Are Solved This PR extends the functionality of `GetProviderByID`, `ListProviders` v1 endpoints and `GetIDPByID` v2 endpoint to also return Zitadel IdP when queried. # How the Problems Are Solved - Add `PROVIDER_TYPE_ZITADEL` and a `ZitadelConfig` to `zitadel.idp.v1.ProviderConfig` (v1 APIs). - Add `IDP_TYPE_ZITADEL`, `ZitadelConfig`, `InstanceRolesInfo` in v2 `idp.proto` - Extend internal/query IDP template querying to include a `ZitadelIDPTemplate` (incl. issuer, client credentials, scopes, instance roles info). - Add/extend integration tests for updating + fetching providers by ID and listing providers. # Additional Changes N/A # Additional Context Closes https://github.com/zitadel/zitadel/issues/12051 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
39ad66bded |
fix: correct scope validation in token exchange (#12312)
## Which Problems Are Solved The v4.15.3 token exchange hardening (`#12319`, `#12322`) introduced two regressions: 1. **Logic bug**: scope validation used intersection (`!inSubject || !inActor`) instead of union, so any explicit `scope` on a `user_id` subject always failed — even `openid` when present on the actor token. 2. **Design gap**: even with union logic, scope-less subjects (`user_id`, `id_token`) cannot satisfy a rule that requires all requested scopes to exist on input tokens. This breaks documented impersonation flows (e.g. service account actor + `user_id` subject requesting `email`). ## How the Problems Are Solved - **Standard exchange** (no actor, or subject carries scopes): keep union validation — requested scopes must be ⊆ subject ∪ actor. For plain exchange this is effectively a subset of the subject token. - **Scope-less subject impersonation** (`user_id`, `id_token` on actor path): split validation: - **Subject-data scopes** (`openid`, `profile`, `email`, …): client allowlist only - **Authorization scopes** (`offline_access`, `:aud`, `projects:roles`, `role:*`): still ⊆ subject ∪ actor - Restore subject → actor fallback when `scope` is omitted. GHSA protections remain: cross-client token binding and authorization-scope escalation are still rejected. Closes #12319 Closes #12322 ## Additional Changes - Unit tests for scope validators (`token_exchange_test.go`) - Integration tests covering `user_id`/`id_token` paths, union-path rejection, and client-credentials actor → `user_id` exchange ## Additional Context - Relates to https://github.com/zitadel/zitadel/releases/tag/v4.15.3 - Security fix: GHSA-vrh8-c9cm-wh8v --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
9d60e83d6f |
Merge commit from fork
* Use slices.Contains over custom function * Correctly remove roles from granted roles * fix(setup): repair user grants with stale roles (GHSA-v859-c572-qh5p) Add setup step 73 that reconciles existing user grants whose roles were left too broad by the buggy cascade removal in removeRoleFromUserGrant. The corruption lives in the eventstore event payloads, so the step pushes a corrective user.grant.cascade.changed event per affected grant (roles intersected with the currently valid set) and re-triggers the user grant projection. Runs in the second setup slice, after the projection tables it reads have been created. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(setup): scope GHSA-v859-c572-qh5p repair to grant-based user grants Direct user grants can never be hit by this bug (only ChangeProjectGrant's multi-role cascade to grant-based grants can trigger it), so drop the direct-grant branch from the finder query to avoid stripping unrelated, legitimate roles that merely mismatch for other reasons (e.g. stale role_key drift). Also exclude removed instances from the migration scope, and log the number of grants fixed per instance. Co-authored-by: Cursor <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: Livio Spring <[email protected]> Co-authored-by: Cursor <[email protected]> |
||
|
|
8b9d3637fd |
chore(deps): bump the go_modules group across 2 directories with 1 update (#12393)
Bumps the go_modules group with 1 update in the /apps/login/acceptance/idp/saml directory: [golang.org/x/crypto](https://github.com/golang/crypto). Bumps the go_modules group with 1 update in the /apps/login/acceptance/samlsp directory: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.36.0 to 0.52.0 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/golang/crypto/commit/a1c0d9929856c8aba2b31f079340f00578eda803"><code>a1c0d99</code></a> go.mod: update golang.org/x dependencies</li> <li><a href="https://github.com/golang/crypto/commit/3c7c86938f4541c333d506f719388d9c42d4763d"><code>3c7c869</code></a> ssh: fix deadlock on unexpected channel responses</li> <li><a href="https://github.com/golang/crypto/commit/533fb3f7e4a5ae23f69d1837cd851d35ff5b76ce"><code>533fb3f</code></a> ssh: fix source-address critical option bypass</li> <li><a href="https://github.com/golang/crypto/commit/abbc44d451a6f9236a2bbd26cbcd4d0fec473da3"><code>abbc44d</code></a> ssh: fix incorrect operator order</li> <li><a href="https://github.com/golang/crypto/commit/e052873987615dc96fe67607a9a6adb76311344f"><code>e052873</code></a> ssh: fix infinite loop on large channel writes due to integer overflow</li> <li><a href="https://github.com/golang/crypto/commit/b61cf853a89d82cad68da5e12a6beca2116f8456"><code>b61cf85</code></a> ssh: enforce user presence verification for security keys</li> <li><a href="https://github.com/golang/crypto/commit/9c2cd33e8d96a96133fd6ff732510ebba539c2bd"><code>9c2cd33</code></a> ssh: enforce strict limits on DSA key parameters</li> <li><a href="https://github.com/golang/crypto/commit/890731877d85f71cfdc9554e7a27fec4684fc4c4"><code>8907318</code></a> ssh: reject RSA keys with excessively large moduli</li> <li><a href="https://github.com/golang/crypto/commit/ffd87b4878fa98ca2908ec534e1a410bf095a35e"><code>ffd87b4</code></a> ssh: fix panic when authority callbacks are nil</li> <li><a href="https://github.com/golang/crypto/commit/4e7a7384ecbc8d519f6f4c11b36fa9d761fc8946"><code>4e7a738</code></a> ssh: fix deadlock on unexpected global responses</li> <li>Additional commits viewable in <a href="https://github.com/golang/crypto/compare/v0.36.0...v0.52.0">compare view</a></li> </ul> </details> <br /> Updates `golang.org/x/crypto` from 0.36.0 to 0.52.0 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/golang/crypto/commit/a1c0d9929856c8aba2b31f079340f00578eda803"><code>a1c0d99</code></a> go.mod: update golang.org/x dependencies</li> <li><a href="https://github.com/golang/crypto/commit/3c7c86938f4541c333d506f719388d9c42d4763d"><code>3c7c869</code></a> ssh: fix deadlock on unexpected channel responses</li> <li><a href="https://github.com/golang/crypto/commit/533fb3f7e4a5ae23f69d1837cd851d35ff5b76ce"><code>533fb3f</code></a> ssh: fix source-address critical option bypass</li> <li><a href="https://github.com/golang/crypto/commit/abbc44d451a6f9236a2bbd26cbcd4d0fec473da3"><code>abbc44d</code></a> ssh: fix incorrect operator order</li> <li><a href="https://github.com/golang/crypto/commit/e052873987615dc96fe67607a9a6adb76311344f"><code>e052873</code></a> ssh: fix infinite loop on large channel writes due to integer overflow</li> <li><a href="https://github.com/golang/crypto/commit/b61cf853a89d82cad68da5e12a6beca2116f8456"><code>b61cf85</code></a> ssh: enforce user presence verification for security keys</li> <li><a href="https://github.com/golang/crypto/commit/9c2cd33e8d96a96133fd6ff732510ebba539c2bd"><code>9c2cd33</code></a> ssh: enforce strict limits on DSA key parameters</li> <li><a href="https://github.com/golang/crypto/commit/890731877d85f71cfdc9554e7a27fec4684fc4c4"><code>8907318</code></a> ssh: reject RSA keys with excessively large moduli</li> <li><a href="https://github.com/golang/crypto/commit/ffd87b4878fa98ca2908ec534e1a410bf095a35e"><code>ffd87b4</code></a> ssh: fix panic when authority callbacks are nil</li> <li><a href="https://github.com/golang/crypto/commit/4e7a7384ecbc8d519f6f4c11b36fa9d761fc8946"><code>4e7a738</code></a> ssh: fix deadlock on unexpected global responses</li> <li>Additional commits viewable in <a href="https://github.com/golang/crypto/compare/v0.36.0...v0.52.0">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/zitadel/zitadel/network/alerts). </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
babbd9e617 |
chore: Upgrade golang.org/x/net to v0.56.0 (#12392)
# Which Problems Are Solved Go Net HTML parser is vulnerable to denial of service. # How the Problems Are Solved `go get -u golang.org/x/net` # Additional Context - Closes https://github.com/zitadel/zitadel/security/dependabot/749 |
||
|
|
43ff093194 |
chore(deps): add devcontainer lockfile (#12362)
# Which Problems Are Solved VSCode auto-generated a devcontainer lockfile during container start. This seems to be a new feature. # How the Problems Are Solved Commit the file # Additional Context https://github.com/devcontainers/spec/blob/main/docs/specs/devcontainer-lockfile.md Co-authored-by: Florian Forster <[email protected]> |
||
|
|
7baa7536ce |
chore: edit issue templates to encourage GitHub account linking for Cloud customers (#12391)
To help us better identify and prioritize issues and requests from our paying customers, this PR adds a quick callout to the top of our GitHub issue templates. It prompts users with active ZITADEL Cloud subscriptions to link their GitHub accounts to their Cloud accounts via the Customer Portal. --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
d15b4e2da2 |
docs: create knowledge gap ID 34 (#12388)
Automatically generated PR resolving Knowledge Gap ID 34. **Thread ID:** manual-1783339162398 **Action:** CREATE **New File:** `content/docs/drafts/gap-manual-1783339162398.mdx` ### AI Summary > Add a user migration guide for firebase to Zitadel ### Human Reviewer Instructions > Firebase to Zitadel Migration Summary > The Blocker: Incompatible Password Hashes > > Firebase uses a proprietary, modified scrypt algorithm requiring project-specific keys. > > Zitadel supports standard algorithms but lacks a verifier for Firebase's custom format. > > Result: Passwords cannot be directly imported. Firebase hashes must be discarded. > > Strategy 1: Bulk Import & Password Reset (Standard) > Requires users to set a new password on their first login. > > Export: Run firebase auth:export users.json --format=json. > > Map: Convert Firebase fields (e.g., localId) to Zitadel's schema. > > Import to Zitadel: Call the Zitadel import API. > > Action: Omit the hashedPassword object entirely. > > Action: Include "passwordChangeRequired": true in the JSON payload to trigger a reset flow via email or login prompt. > > Strategy 2: Just-In-Time (JIT) Migration (Seamless) > Migrates users transparently behind the scenes during an active grace period. > > Intercept Login: Your backend captures the plain-text password during login. > > Verify: Backend POSTs credentials to Firebase Auth REST API: > > https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=[WEB_API_KEY] > > Migrate to Zitadel: > > If Valid (200 OK): Create the user in Zitadel immediately using the plain-text password (Zitadel will natively hash it). > > If Invalid (400): Reject login or check if the user is already in Zitadel. > > Sunset: After the grace period ends, migrate remaining inactive users using Strategy 1. > (Note: Do not log plain-text passwords and strictly enforce HTTPS during this phase). --- 🤖 **Need adjustments?** Leave a comment below and tag **@zitadel-knowledge-bot** with your requested changes, and I will automatically update the files and push a new commit! --------- Co-authored-by: zitadel-knowledge-bot[bot] <296006658+zitadel-knowledge-bot[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
942607e0e9 |
feat: implement UpdateZitadelProvider in ManagementService (#12384)
# Which Problems Are Solved This PR adds support for updating ZITADEL identity provider (IDP) templates. # How the Problems Are Solved - Implements `UpdateZitadelProvider` in the Management gRPC server plus converter - Introduces an org-scoped `ZitadelIDPChanged` event and registers its event mapper. - Adds command-side support to update org Zitadel providers and appends the new event to the org write model. - Extends the IDP template projection reducer to accept org change events. # Additional Changes N/A # Additional Context Closes https://github.com/zitadel/zitadel/issues/11922 Follow up for PRs: - https://github.com/zitadel/zitadel/pull/12018 - https://github.com/zitadel/zitadel/pull/12020 - https://github.com/zitadel/zitadel/pull/12055 - https://github.com/zitadel/zitadel/pull/12056 - https://github.com/zitadel/zitadel/pull/12371 - https://github.com/zitadel/zitadel/pull/12378 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Livio Spring <[email protected]> |
||
|
|
bc9333aef9 |
chore: update npm dependencies (#12375)
# Which Problems Are Solved - npm dependencies across the monorepo are behind current patch/minor releases. - Transitive dependencies are pinned to older versions by parent packages (karma, nx, @changesets/cli, etc.). - Console build fails after the Angular toolchain update because `angular.json` references assets outside the workspace root. # How the Problems Are Solved - Bumps `@angular/*` to `^21.2.17` in console. - Bumps `js-yaml` to `^4.2.0` in docs. - Bumps `concurrently` to `^10.0.3` in login. - Adds pnpm overrides for transitive deps that cannot be bumped directly (ws, undici, minimatch, esbuild, dompurify, qs, and others). # Additional Changes - Removes 10 overrides that are no longer needed after parent packages resolve to newer versions. - Updates 4 existing overrides (`tar`, `js-yaml`, `dompurify`, `brace-expansion`) to match current upstream ranges. - 2 low-severity findings remain via the abandoned `raw-loader` package in docs (peer dep resolution; no upstream fix without replacing `raw-loader`). - Fixes console build: replaced the `angular.json` asset glob `../apps/docs/public/img/tech` with a `prebuild` script that copies tech images into `src/assets/docs/img/tech`. **Verify at runtime that tech images on project grant / integration pages still load.** # Additional Context - Overrides remain where parent packages still pin older transitive versions. - The `angular.json` asset path issue predates this PR (not introduced by the Angular bump). --------- Co-authored-by: Livio Spring <[email protected]> |
||
|
|
653a968d91 |
feat: implement instance-level UpdateZitadelProvider (#12378)
# Which Problems Are Solved This PR implements the instance-level `UpdateZitadelProvider` endpoint, enabling updates to existing instance Zitadel IdP templates. # How the Problems Are Solved - Added `AdminService.UpdateZitadelProvider` handler, converter - Added the command-layer implementation to update a ZitadelProvider and push `instance.idp.zitadel.changed` event - Added projection-layer implementation to persist Zitadel provider updates into `projections.idp_templates6_zitadel` - Added unit and integration tests # Additional Changes N/A # Additional Context Related to https://github.com/zitadel/zitadel/issues/11922 Follow-up for PRs: - https://github.com/zitadel/zitadel/pull/12018 - https://github.com/zitadel/zitadel/pull/12020 - https://github.com/zitadel/zitadel/pull/12055 - https://github.com/zitadel/zitadel/pull/12056 - https://github.com/zitadel/zitadel/pull/12371 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
1beabed8cf |
fix(login): preserve org domain suffix through account chooser navigation (#12304)
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]> |
||
|
|
6b629c2290 |
docs: update roadmap and remove release cycle page (#12373)
Removes the release cycle page and sidebar entry. Replaces the roadmap page with updated strategic roadmap content and renames its sidebar label to Roadmap. --------- Co-authored-by: Elina Sokolovska <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Fabienne Bühler <[email protected]> |