mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-02 12:22:15 +00:00
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]>
This commit is contained in:
@@ -105,9 +105,17 @@ func call(w http.ResponseWriter, req *http.Request) {
|
||||
// build the response from the received response
|
||||
resp := request.Response
|
||||
// manipulate the received response to send back as response
|
||||
if resp != nil && resp.AddHumanUser != nil {
|
||||
// manipulate the response
|
||||
resp.AddHumanUser.Metadata = append(resp.AddHumanUser.Metadata, &user.SetMetadataEntry{Key: "organization", Value: []byte("company")})
|
||||
if resp != nil {
|
||||
// On the first login the user does not exist yet, so the response contains a CreateUser
|
||||
// action. On subsequent logins the user already exists and the response contains an
|
||||
// UpdateUser action instead. Both support setting user metadata directly.
|
||||
metadata := &user.Metadata{Key: "organization", Value: []byte("company")}
|
||||
if createUser := resp.GetCreateUser(); createUser != nil {
|
||||
createUser.Metadata = append(createUser.Metadata, metadata)
|
||||
}
|
||||
if updateUser := resp.GetUpdateUser(); updateUser != nil {
|
||||
updateUser.Metadata = append(updateUser.Metadata, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
// marshal the response into json
|
||||
@@ -189,6 +197,14 @@ curl -L -X PUT 'https://${CUSTOM_DOMAIN}/v2/actions/executions' \
|
||||
|
||||
Now that you have set up the target and execution, you can test it by using a login-flow in the typescript login with an external IDP.
|
||||
|
||||
<Callout>
|
||||
When the user logs in for the first time and does not exist yet, the response contains a `createUser` object (matching the [Create User](/reference/api/user/zitadel.user.v2.UserService.CreateUser) request).
|
||||
When the user already exists, the response instead contains an `updateUser` object (matching the [Update User](/reference/api/user/zitadel.user.v2.UserService.UpdateUser) request).
|
||||
Both allow setting user `metadata` directly in the same response.
|
||||
|
||||
The previously used `addHumanUser` and `updateHumanUser` objects are deprecated and replaced by `createUser` and `updateUser` respectively.
|
||||
</Callout>
|
||||
|
||||
```json
|
||||
{
|
||||
"details": {
|
||||
@@ -218,20 +234,22 @@ Now that you have set up the target and execution, you can test it by using a lo
|
||||
}
|
||||
}
|
||||
},
|
||||
"addHumanUser": {
|
||||
"idpLinks": [
|
||||
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
|
||||
],
|
||||
"createUser": {
|
||||
"username": "test-user@localhost",
|
||||
"profile": {
|
||||
"givenName": "Test",
|
||||
"familyName": "User",
|
||||
"displayName": "Test User",
|
||||
"preferredLanguage": "de"
|
||||
},
|
||||
"email": {
|
||||
"email": "test-user@zitadel.ch",
|
||||
"isVerified": true
|
||||
"human": {
|
||||
"idpLinks": [
|
||||
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
|
||||
],
|
||||
"profile": {
|
||||
"givenName": "Test",
|
||||
"familyName": "User",
|
||||
"displayName": "Test User",
|
||||
"preferredLanguage": "de"
|
||||
},
|
||||
"email": {
|
||||
"email": "[email protected]",
|
||||
"isVerified": true
|
||||
}
|
||||
},
|
||||
"metadata": []
|
||||
}
|
||||
@@ -270,20 +288,22 @@ the [Sent information Response](./usage#sent-information-response) payload descr
|
||||
}
|
||||
}
|
||||
},
|
||||
"addHumanUser": {
|
||||
"idpLinks": [
|
||||
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
|
||||
],
|
||||
"createUser": {
|
||||
"username": "test-user@localhost",
|
||||
"profile": {
|
||||
"givenName": "Test",
|
||||
"familyName": "User",
|
||||
"displayName": "Test User",
|
||||
"preferredLanguage": "de"
|
||||
},
|
||||
"email": {
|
||||
"email": "test-user@zitadel.ch",
|
||||
"isVerified": true
|
||||
"human": {
|
||||
"idpLinks": [
|
||||
{"idpId": "218528353504723201", "userId": "218528353504723202", "userName": "test-user@localhost"}
|
||||
],
|
||||
"profile": {
|
||||
"givenName": "Test",
|
||||
"familyName": "User",
|
||||
"displayName": "Test User",
|
||||
"preferredLanguage": "de"
|
||||
},
|
||||
"email": {
|
||||
"email": "[email protected]",
|
||||
"isVerified": true
|
||||
}
|
||||
},
|
||||
"metadata": [
|
||||
{"key": "organization", "value": "Y29tcGFueQ=="}
|
||||
@@ -299,7 +319,7 @@ The same `RetrieveIdentityProviderIntent` webhook is useful for diagnosing auto-
|
||||
A common symptom is an error like:
|
||||
|
||||
```
|
||||
invalid AddHumanUserRequest.Profile: embedded message failed validation |
|
||||
invalid CreateUserRequest.Human.Profile: embedded message failed validation |
|
||||
caused by: invalid SetHumanProfile.GivenName: value length must be between 1 and 200 runes, inclusive
|
||||
```
|
||||
|
||||
@@ -307,7 +327,7 @@ This means ZITADEL received an empty value for `givenName` during auto-creation.
|
||||
|
||||
**To inspect the raw claims ZITADEL receives from your IdP**, point the execution at a logging endpoint (such as [Webhook.site](./webhook-site-setup)) instead of a manipulation handler. Trigger a login with the external IdP and look at the `rawInformation` field in the payload — this contains the unmodified claims exactly as ZITADEL received them.
|
||||
|
||||
Compare the claim keys in `rawInformation` against what ZITADEL maps to standard profile fields. If the IdP uses a non-standard key, you can remap it in the same webhook by setting `addHumanUser.profile.givenName` from the correct source field before returning the response.
|
||||
Compare the claim keys in `rawInformation` against what ZITADEL maps to standard profile fields. If the IdP uses a non-standard key, you can remap it in the same webhook by setting `createUser.human.profile.givenName` from the correct source field before returning the response.
|
||||
|
||||
## Conclusion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user