* fix(command): enforce permission check when issuing passkey enrollment codes
addUserPasskeyCode - behind CreatePasskeyRegistrationLink via
AddUserPasskeyCode / AddUserPasskeyCodeURLTemplate / AddUserPasskeyCodeReturn -
issued a passkey enrollment code without any authorization check.
The RPC's auth annotation carries user.passkey.write with no org_field, so the
API interceptor only verifies the caller's permission in the org taken from the
caller-supplied x-zitadel-orgid header, never the org that owns the target user.
The gRPC handlers additionally pass an empty resourceOwner, and the write model
lookup is instance-wide, so the code was minted against the victim's aggregate
regardless of org. An org owner of one organization could therefore issue a
passkey enrollment code for a user in another organization, enroll an
attacker-controlled authenticator and take over the account.
Authorize against the target user's actual resource owner, resolved from the
write model, before pushing the code requested event.
The check uses user.passkey.write - the permission the RPC already declares -
rather than user.credential.write used by the sibling RegisterUserPasskey. This
narrows the organization the permission is evaluated in without narrowing the
set of roles that may call the endpoint: IAM_USER_MANAGER holds
user.passkey.write but not user.credential.write, and would otherwise lose the
endpoint instance-wide.
The enrollment code stays a bearer credential by design, so the unauthenticated
login flows redeeming it are unaffected; issuance is the only place this can be
enforced.
Co-Authored-By: Claude Opus 5 <[email protected]>
* test(command): guard cross-org passkey enrollment code issuance
The existing AddUserPasskeyCode* tests all use an allowed permission check, so
none of them fails if the authorization is removed again.
Assert the denial path instead: with the caller passing an empty resourceOwner,
as the v2 gRPC handlers do, the check must receive the target user's real
resource owner resolved from the write model, and no event may be pushed.
Without the check in addUserPasskeyCode all three subtests fail, one of them on
an unexpected Push - a code would actually have been issued.
Co-Authored-By: Claude Opus 5 <[email protected]>
* test(user/v2): simulate cross-org passkey registration link attack
End-to-end reproduction against a live instance: an org owner of one
organization requests a passkey registration link, with the request-header org
pinned to its own org, for a user that lives in another organization. Uses the
return-code medium so a successful response would carry the plaintext code,
making a missing denial impossible to pass silently.
The second subtest pins the other half of the contract: IAM_USER_MANAGER holds
user.passkey.write instance-wide and must keep working across organizations, so
the fix cannot be tightened into user.credential.write without regressing it.
No equivalent test for user/v2beta: it shares the same internal/command layer
and is fixed by the same change, but the API is deprecated, so the duplicate is
deliberately omitted.
Co-Authored-By: Claude Opus 5 <[email protected]>
* refactor(command): generate passkey code only after the permission check
Review feedback: the code ID and the passkey code - the latter costs an extra
eventstore filter for the secret generator config - were produced before the
permission check, so an unauthorized caller still paid for both.
The write model is now built with an empty code ID, which is equivalent for
the lookup: the ID only matches events of an already existing code, and a code
about to be created has none. It is assigned right after the check so the
pushed event is still appended to the write model.
Co-Authored-By: Claude Opus 5 <[email protected]>
* fix(command): enforce permission check on v1 passwordless init codes
The deprecated management RPCs AddPasswordlessRegistration and
SendPasswordlessRegistration accept an attacker-controlled user_id and pass the
caller's own organization to the command. That organization only scopes the
write model's read, not the write: for a user of another organization the read
matches nothing, the write model keeps the caller's organization, and on push
the eventstore re-owns the event to the target's real organization
(internal/eventstore/v3/sequence.go). An org admin could therefore mint a
passwordless enrollment link for a user of any other organization and, after
correcting the orgID in the returned link, take the account over.
Same class of flaw as the v2 CreatePasskeyRegistrationLink issue, but a
different command, so the earlier fix does not reach it. Copying that fix
verbatim would not close it either: authorizing against the write model's
resource owner would authorize against the attacker's own organization, where
they legitimately hold the permission. The check is instead handed an empty
resource owner, which makes it resolve the target's real owner from the
eventstore.
The check sits in the two exported commands rather than in the shared inner
function, for two reasons:
- the RPCs declare different permissions, user.credential.write and
user.write. Hardcoding one would either lock IAM_USER_MANAGER and
ORG_USER_MANAGER out of SendPasswordlessRegistration or widen
AddPasswordlessRegistration to them. Access is preserved exactly.
- the inner function is also called by importHuman while creating a user,
whose aggregate has no events yet and whose owner cannot be resolved.
Self management stays allowed, so the auth API's AddMyPasswordlessLink and
SendMyPasswordlessLink are unaffected, as is anonymous redemption of the code
through the login UI - the init code is a bearer credential by design and
issuance is the only place this can be enforced.
Co-Authored-By: Claude Opus 5 <[email protected]>
* test(command): guard cross-org v1 passwordless init code issuance
Asserts the organization handed to checkPermission, not merely that a denial
happened. A caller acting in org1 targeting a user of org2 must be authorized
against org2; a test that only asserted PermissionDenied would pass against the
attacker's own organization, where they hold the permission - which is exactly
how a fix copied from the v2 command would fail.
Also pins the permission per command, so a later attempt to hoist the check
into the shared inner function shows up as roles gaining or losing access, and
asserts that self management still skips the check for the auth API.
Negative controls, both confirmed:
- authorizing against the caller-supplied resource owner: the two RPC
subtests fail on org1 != org2.
- dropping the check: they fail on an unexpected ID generation and an
unfulfilled resource owner lookup, i.e. a code would have been issued.
Co-Authored-By: Claude Opus 5 <[email protected]>
* test(management): simulate cross-org passwordless registration link attack
Reproduces the attack against a live instance through the deprecated v1
management API, and pins the two access guarantees the fix has to keep.
Verified by running the three builds against a real instance:
| build | cross-org denied | IAM_USER_MANAGER can send |
|--------------------------------|------------------|---------------------------|
| no check | FAIL, link with | PASS |
| | plaintext code | |
| | returned | |
| user.credential.write for both | PASS | FAIL, AUTH-AWfge |
| per-RPC permissions (shipped) | PASS | PASS |
Row 1 is the vulnerability. Row 2 is why SendPasswordlessRegistration keeps
user.write: IAM_USER_MANAGER holds it instance wide, and instance memberships
resolve in every organization, so the stricter permission would take the
endpoint away from that role everywhere.
ORG_USER_MANAGER gets no separate case: it holds the same user.write through an
org-scoped membership, which the same-org subtest already covers. The auth API's
self-service equivalents have no integration test suite; they are covered by the
self-management unit test instead.
Co-Authored-By: Claude Opus 5 <[email protected]>
* chore: trigger checks
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
# Which Problems Are Solved
As part of #11035 , this PR implements the Passkey check logic for
session validation
# How the Problems Are Solved
- Refactor webauth FinishLogin to support new domain model
- Add webauth config to defaults
- Implement passkey check logic and tests
- Manual transaction management to avoid stalling the DB while
FinishLogin callback is executed
- Update passkey Type condition to allow passing a text operation
(equal, contains, etc..)
# Additional Context
This is a cherry-picked PR + minor changes, coming from
https://github.com/zitadel/zitadel/pull/11164
- Relates to #11035
---------
Co-authored-by: Fabienne Bühler <[email protected]>
Co-authored-by: Gayathri Vijayan <[email protected]>
When an expired invite code was used for webauthn (passkeys), the change date got updated by the failed event. This change date was used to test for expiry, meaning failed events would reset the expiry timeout.
This fix adds a Code Creation Date to the writemodel which gets set by the first event. This can be the added or requested event.
Other changes:
- Expiry renamed to CodeExpiry so it's consistent with similar write models using secret codes.
- humanVerifyPasswordlessInitCode takes an algorithm instead of the complete generator, so the method can be unit tested easier
- Added tests that reproduced the original issue
# Which Problems Are Solved
This PR fixes the self-management of users for metadata and own removal
and improves the corresponding permission checks.
While looking into the problems, I also noticed that there's a bug in
the metadata mapping when using `api.metadata.push` in actions v1 and
that re-adding a previously existing key after its removal was not
possible.
# How the Problems Are Solved
- Added a parameter `allowSelfManagement` to checkPermissionOnUser to
not require a permission if a user is changing its own data.
- Updated use of `NewPermissionCheckUserWrite` including prevention of
self-management for metadata.
- Pass permission check to the command side (for metadata functions) to
allow it implicitly for login v1 and actions v1.
- Use of json.Marshal for the metadata mapping (as with
`AppendMetadata`)
- Check the metadata state when comparing the value.
# Additional Changes
- added a variadic `roles` parameter to the `CreateOrgMembership`
integration test helper function to allow defining specific roles.
# Additional Context
- noted internally while testing v4.1.x
- requires backport to v4.x
- closes https://github.com/zitadel/zitadel/issues/10470
- relates to https://github.com/zitadel/zitadel/pull/10426
# Which Problems Are Solved
Some user v2 API calls checked for permission only on the user itself.
# How the Problems Are Solved
Consistent check for permissions on user v2 API.
# Additional Changes
None
# Additional Context
Closes#7944
---------
Co-authored-by: Livio Spring <[email protected]>
# Which Problems Are Solved
The v2beta services are stable but not GA.
# How the Problems Are Solved
The v2beta services are copied to v2. The corresponding v1 and v2beta
services are deprecated.
# Additional Context
Closes#7236
---------
Co-authored-by: Elio Bischof <[email protected]>
* feat: user service v2 remove user
* feat: user service v2 add user human
* feat: user service v2 change user human
* feat: user service v2 change user human unit tests
* feat: user service v2 reactivate, deactivate, lock, unlock user
* feat: user service v2 integration tests
* fix: merge back origin/main
* lint: linter corrections
* fix: move permission check for isVerfied and password change
* fix: add deprecated notices and other review comments
* fix: consistent naming in proto
* fix: errors package renaming
* fix: remove / delete user renaming in integration test
* fix: machine user status changes through user v2 api
* fix: linting changes
* fix: linting changes
* fix: changes from review
* fix: changes from review
* fix: changes from review
* fix: changes from review
* fix: changes from review
---------
Co-authored-by: Tim Möhlmann <[email protected]>
Co-authored-by: Livio Spring <[email protected]>
This fix provides a possibility to pass a domain on the session, which
will be used (as rpID) to create a passkey / u2f assertion and
attestation. This is useful in cases where the login UI is served under
a different domain / origin than the ZITADEL API.
* command/crypto: DRY the code
- reuse the the algorithm switch to create a secret generator
- add a verifyCryptoCode function
* command: crypto code tests
* migrate webauthn package
* finish integration tests with webauthn mock client
* fix: rename iam command side to instance
* fix: rename iam command side to instance
* fix: rename iam command side to instance
* fix: rename iam command side to instance
* fix: rename orgiampolicy to domain policy
* fix: merge conflicts
* fix: protos
* fix: md files
* implement deprecated org iam policy again
Co-authored-by: Livio Amstutz <[email protected]>
* begin pw less registration
* create pwless one time codes
* send pwless link
* separate send and add passwordless link
* separate send and add passwordless link events
* custom message text for passwordless registration
* begin custom login texts for passwordless
* i18n
* i18n message
* i18n message
* custom message text
* custom login text
* org design and texts
* create link in human import process
* fix import human tests
* begin passwordless init required step
* passwordless init
* passwordless init
* do not return link in mgmt api
* prompt
* passwordless init only (no additional prompt)
* cleanup
* cleanup
* add passwordless prompt to custom login text
* increase init code complexity
* fix grpc
* cleanup
* fix and add some cases for nextStep tests
* fix tests
* Update internal/notification/static/i18n/en.yaml
* Update internal/notification/static/i18n/de.yaml
* Update proto/zitadel/management.proto
* Update internal/ui/login/static/i18n/de.yaml
* Update internal/ui/login/static/i18n/de.yaml
* Update internal/ui/login/static/i18n/de.yaml
Co-authored-by: Fabi <[email protected]>
* fix: adaot config to commands (and queries)
* remove dependency on vv2 in v1
* add queries user to operator
* set password for queries on tests
* set password for queries on tests
* fix config