Commit Graph
4618 Commits
Author SHA1 Message Date
464d1cb655 fix(login): resolve unknownContext error on password page caused by inconsistent ignoreUnknownUsernames handling (#12512)
# Which Problems Are Solved

The `/password` page (and related password pages) can show the
`unknownContext` error even though the user was resolved correctly and
login completes fine. Two causes:

1. `sendLoginname` used two different `ignoreUnknownUsernames` sources:
session creation was gated on the **user's org** login settings, while
the redirect's `loginName` masking and enumeration guards were gated on
a **caller-supplied flag** reflecting the request context (instance or
org scope). Whenever the two diverge — e.g. `ignoreUnknownUsernames`
enabled on the instance but not on the user's org, or the reverse — the
redirect carries a `loginName` that doesn't match the session cookie (or
no session is created at all), so the `/password` page can't load the
session and renders `unknownContext`, while password submission still
succeeds via its user-search fallback. This affects every caller of
`sendLoginname` — the manual `/loginname` form as well as the
server-side `login_hint` resolution; it was most reproducible with
`login_hint`, since that path resolves context settings at the instance
level, maximizing the chance of divergence with an org-level policy.

2. The password pages tried to re-derive "was enumeration protection
applied?" from login settings to suppress the error — but they resolve
settings from a different context than `sendLoginname` (falling back to
the default organization instead of the instance), so any org-level
custom policy made the suppression misfire. More fundamentally, the
alert was keyed on a failed session lookup, which is not an error state:
under enumeration protection no session exists by design, and the forms
recover without one (`sendPassword` falls back to user search;
`/password/set` works via code + userId).

# How the Problems Are Solved

`sendLoginname` no longer accepts `ignoreUnknownUsernames` from the
caller. It derives a single constant server-side from the
request-context login settings it already fetches and uses it for every
decision in the flow: session creation, `preventUserEnumeration`, the
multiple-users branch, the INITIAL-user check, the organization reset,
the auth-method guards, and the IdP redirect after domain discovery.
Redirect params use `session?.factors?.user?.loginName ??
command.loginName`: when a session exists, its loginName goes in the URL
so the next page always finds the session cookie; when protection
applies, no session (and no session cookie) is created and the raw input
is echoed, keeping known and unknown users fully indistinguishable —
including the previously observable `Set-Cookie` difference.
`UsernameForm` and `resolveLoginHint` stop forwarding the flag, which
also removes the redundant `getLoginSettings` call in
`resolveLoginHint`.

The password pages no longer consult login settings or the session
lookup for the `unknownContext` error and instead warn only when
required input is actually missing (matching the existing pattern on the
passkey/mfa/u2f pages): `/password` and `/password/change` when no
`loginName` searchParam is present, `/password/set` when neither
`loginName` nor `userId` is present. A missing session alone no longer
triggers the error.

Verified against a live instance in all four settings combinations
(uniform on/off, both divergence directions) including a
known-vs-unknown `login_hint` comparison producing byte-identical
responses under protection; full unit suite passes.

---------

Co-authored-by: Livio Spring <[email protected]>
2026-08-04 12:49:28 +00:00
jmaretteandGitHub 9070b537d2 feat(oidc): add RFC 7592 dynamic client management (#12315)
# Which Problems Are Solved

#12313 lets clients register themselves as OIDC applications (RFC 7591),
but a dynamically registered client has no way to manage itself
afterwards:

- The registration response returns no `registration_access_token` and
no `registration_client_uri`, which RFC 7591 and 7592 clients (including
the MCP SDKs) expect in order to manage their own registration.
- There is no way to read back, update or delete a registration without
an operator acting on the client's behalf through the Console or the
Management API. A client that needs to rotate a redirect URI has to ask
a human.

# How the Problems Are Solved

Implement the [OAuth 2.0 Dynamic Client Registration Management Protocol
(RFC 7592)](https://datatracker.ietf.org/doc/html/rfc7592): `GET`, `PUT`
and `DELETE` on `/oauth/v2/register/{client_id}`, wired through the same
`op.WithSetRouter` hook as the registration endpoint.

## Control plane: the same security settings, no new switch

The management endpoints are gated on the
`DynamicClientRegistrationSettings.enabled` introduced in #12313. No new
setting, no feature flag: a client that may register may manage what it
registered. When registration is disabled the endpoints answer `404`,
exactly as `POST /oauth/v2/register` does.

`allow_unauthenticated` selects the registration mode and does not apply
here, because the registration access token authorizes these endpoints
in both modes.

## Authorization: the registration access token, and nothing else

RFC 7592 §3 defines the credential as the registration access token
issued at registration and bound to a single client, not a user
credential. So a client manages only itself, proving it with the token
it was handed:

| | Registration (`POST`) | Management (`GET` / `PUT` / `DELETE`) |
| --- | --- | --- |
| Credential | Access token, or none in open mode | Registration access
token |
| Permission | `project.app.register_dynamic` in token mode | None |

Neither `project.app.register_dynamic`, which governs who may *create*
clients, nor `project.app.write` applies to the management endpoints.
One authorization path per endpoint is easier to reason about than two,
and there is no user identity to check against in the first place.
Operators keep managing dynamically registered clients through the
Management API and the Console, like any other application; they cannot
obtain a client's registration access token, by design.

Status codes: a missing, malformed or non matching token is `401`; a
client that no longer exists is `404`. The token's `client_id` is
checked against the path before any lookup, so a token bound to another
client is rejected without touching the database.

## The token itself

- **Stored like a client secret.** Only its passwap hash is persisted,
through a new
`project.application.config.oidc.registration_token.changed` event; the
plain token leaves the server exactly once, in the response that issues
it.
- **Bound to its client and organization** through authenticated
encryption with the instance key, the same mechanism as refresh tokens.
The opaque token handed to the client encrypts
`client_id:org_id:secret`, so the endpoint recovers the binding before
checking the secret against the stored hash.
- **Verified in O(1)** against the hash on the projection, folded into
the client lookup the handler already does, with a strongly consistent
fallback to the eventstore so a token that was just rotated is accepted
before it is projected.
- **Rotated on update** (RFC 7592 §4.4) and invalidated implicitly when
the client is deleted. A read does not rotate it. Every update rotates,
including one that changes no metadata, so a client always gets a usable
token back.

## Reusing the existing persistence

A small refactor extracts the shared OIDC application creation and
update logic (`pushOIDCApplication` gains a variadic `extraEvents`, and
`oidcApplicationChangeEvent` is lifted out of `UpdateOIDCApplication`),
so the dynamic commands reuse it without the permission check. The
registration token event is pushed atomically with the application, so a
registered client is never left unmanageable.

# Additional Changes

- Setup step **74** adds the `registration_token` column to the
`apps7_oidc_configs` projection, with a reducer for the new event
modelled on the client secret. Applications are not part of the
relational storage projection, so there is no second write path to keep
in sync.
- The registration response (`POST`) now also carries
`registration_access_token` and `registration_client_uri`.
- Docs: a "Manage a registration" section in the dynamic client
registration guide covering the three operations, the authorization
model, token rotation and the new limitations, plus the management
endpoints on the OIDC endpoints page.
- Unit tests for the command layer (issue, verify, rotate, update,
delete), the projection reducer, and the token binding and response
mapping. An integration test covers the full lifecycle (register, read,
update with rotation, delete, read again with `404`), a token bound to
another client, and the endpoints not being served when registration is
disabled. None of the integration calls carries a user access token,
which pins down that the management endpoints require no permission of
their own.

# Additional Context

- Part of the MCP authorization effort in #9810. Follow-up to #12313
(RFC 7591), which is now merged; this branch has been rebased on `main`
and carries the settings and authorization model agreed there.
- **Open question for the review**: the authorization model above
(registration access token only, no operator path through these
endpoints) is the one I proposed when #12313 was reviewed, but it was
never explicitly confirmed. If you would rather have `project.app.write`
also grant access to a client in the operator's own organization, that
is a contained change in `authorizeClientManagement`.
- Rotation is eventually consistent: right after an update the previous
token may keep working for a brief moment until the rotation is
projected, while the new token works immediately through the eventstore
fallback. This matches how ZITADEL validates access tokens, where
revocation is likewise eventually consistent. It is documented in the
guide and asserted as such in the integration test.
- A database migration is introduced for the new projection column. No
proto change and no new feature flag.
- Verified locally: full build, `go vet` including `-tags integration`,
`golangci-lint` with 0 issues, and the unit suites for every touched
package. As before, I cannot run the integration suite here, so CI
remains the check on it.
2026-08-03 08:37:59 +00:00
a8861336f4 fix(api): accept SetSecuritySettings on /v2/settings/security (#12518)
# Which Problems Are Solved

`GetSecuritySettings` is served at `GET /v2/settings/security`, but
`SetSecuritySettings` was only bound to `PUT /v2/policies/security`.
That split made the natural copy-paste path (`PUT
/v2/settings/security`)
return `405`, including the enable curl in the Dynamic Client
Registration
guide.

# How the Problems Are Solved

Make `PUT /v2/settings/security` the primary HTTP binding for
`SetSecuritySettings`, and keep `PUT /v2/policies/security` as an
`additional_bindings` entry so existing callers keep working.

# Additional Changes

None.

# Additional Context

- Noticed during review of the Dynamic Client Registration PR (#12313):
the
guide's enable curl used `PUT /v2/settings/security` and hit `405`
against a
  live instance.
- Only `settings/v2` is changed; `v2beta` is left as-is (deprecated).
- Generated gateway/OpenAPI artifacts are gitignored and rebuilt from
the
  proto on generate.

Co-authored-by: Cursor <[email protected]>
2026-07-31 11:34:00 +02:00
d35da1764d chore: Backlog cleanup/tag stale issues daily (#12522)
# Which Problems Are Solved

Adds a daily scheduled workflow that tags issues stale at 12 months (no
comment, reaction, or cross-reference in that window) with
`To-be-closed` and posts the standard cleanup comment — same rule as the
one-time sweep already run.

Skips issues already labeled `To-be-closed` or `to-be-reviewed`. Tagging
and commenting only — no auto-close.

---------

Co-authored-by: Elina Sokolovska <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
2026-07-31 09:17:32 +00:00
8bbf0ad4e0 chore: move commented To-be-closed issues to to-be-reviewed (#12515)
# Which Problems Are Solved

Adds a workflow that moves an issue from `To-be-closed` to
`to-be-reviewed` once it gets a genuine reply (not counting our own
auto-generated cleanup comments or bot comments).

Runs two ways:
- Instantly, when a comment is posted on an issue.
- Daily, as a safety-net sweep of all `To-be-closed` issues, in case the
instant check ever misses one.

This is just the label-swap automation. The 30-day auto-close workflow
is separate and not part of this PR.

---------

Co-authored-by: Elina Sokolovska <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
2026-07-31 09:03:56 +00:00
dc1c8620bc fix(login): provide hidden username on password set/change forms (#12490)
<!--
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 set-password, change-password and set-register-password forms render
only
password fields — there is no username field inside the `<form>`.
Password
  managers look for a username field adjacent to the
password field in order to associate and save the credential. Without
one, on
these screens they either skip offering to save, or save a password
entry with
no username attached — so the saved record can't later be matched to the
account.

The existing sign-in form (`password-form.tsx`) already carries a
username field
  for this reason; the password *creation/change* flows were missing the
  equivalent.

  # How the Problems Are Solved

Add a username field to each of the three forms so the newly set
password is
  saved against the correct account:

- `set-password-form.tsx` and `change-password-form.tsx` use the
`loginName`.
  - `set-register-password-form.tsx` uses the `email`.

The field is a real, focusable-excluded `type="text"` input rather than
`type="hidden"` — password managers ignore `type="hidden"` fields when
looking
  for a username, so a visually-hidden text input is required. It is:

  - `readOnly` (avoids a controlled-input warning and user edits),
- `tabIndex={-1}` and `aria-hidden="true"` (kept out of the tab order
and the
    accessibility tree),
  - `className="sr-only"` (visually hidden but present in the DOM),
  - `autoComplete="username"` (tells the password manager what it is).

The value comes from props already passed to each form, so no signatures
change.

  # Additional Changes

  None.

  # Additional Context

- The username values (`loginName` / `email`) are already available in
each
component, so this is a purely presentational change with no server-side
or
    API impact.

Co-authored-by: Max Peintner <[email protected]>
2026-07-31 07:45:01 +00:00
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]>
2026-07-31 09:34:06 +02:00
jmaretteandGitHub 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`).
2026-07-30 14:43:47 +00:00
gayathriandGitHub 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
2026-07-29 15:31:34 +02:00
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]>
2026-07-29 12:06:36 +00:00
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]>
2026-07-29 05:50:40 +02:00
Livio SpringandGitHub fe935d91b8 Merge commit from fork 2026-07-29 05:46:27 +02:00
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]>
2026-07-29 05:45:56 +02:00
gayathriandGitHub 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
2026-07-28 15:20:46 +02:00
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>&nbsp;<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>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Silvan <[email protected]>
2026-07-28 11:56:07 +00:00
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]>
2026-07-27 14:59:02 +00:00
Marco A.andGitHub 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
2026-07-23 08:12:09 +00:00
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]>
2026-07-22 13:47:00 +02:00
Federico CoppedeandGitHub 62e56e085c docs: fixed broken docs references (#12465) 2026-07-20 15:28:34 +02:00
Livio SpringandGitHub 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.
2026-07-20 12:57:32 +00:00
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]>
2026-07-17 13:09:48 +00:00
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]>
2026-07-17 14:26:01 +02:00
Florian ForsterandGitHub 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.
2026-07-17 11:36:51 +00:00
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]>
2026-07-17 13:24:18 +02:00
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]>
2026-07-17 08:37:41 +02:00
Max PeintnerandGitHub 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
2026-07-17 08:23:56 +02:00
Livio SpringandGitHub baf6ed501b Merge commit from fork
* fix(actions): prevent disk access via require

* add test to ensure native modules are still possible
2026-07-17 08:23:31 +02:00
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]>
2026-07-16 16:35:44 +00:00
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]>
2026-07-16 14:42:11 +00:00
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]>
2026-07-16 12:01:07 +02:00
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]>
2026-07-15 15:14:18 +00:00
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]>
2026-07-15 16:47:15 +02:00
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]>
2026-07-15 11:27:25 +00:00
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]>
2026-07-15 04:35:41 +00:00
Tim MöhlmannandGitHub 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.
2026-07-15 06:17:06 +02:00
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]>
2026-07-14 14:16:13 +00:00
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]>
2026-07-14 13:50:09 +02:00
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]>
2026-07-14 09:03:46 +00:00
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]>
2026-07-14 10:05:09 +02:00
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]>
2026-07-13 12:43:47 -03:00
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]>
2026-07-13 11:37:43 +00:00
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]>
2026-07-13 09:22:31 +02:00
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]>
2026-07-10 09:52:56 +00:00
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]>
2026-07-10 09:17:49 +02:00
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]>
2026-07-09 12:33:11 +00:00
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]>
2026-07-09 13:28:09 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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>
2026-07-08 12:46:42 +02:00
Marco A.andGitHub 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
2026-07-08 09:54:39 +00:00
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]>
2026-07-08 05:51:42 +00:00
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]>
2026-07-08 10:55:38 +07:00