fix(user): Updating user info when authenticating with external IDP (#11046)

# Which Problems Are Solved

User profile updates were not propagated when using External OIDC IDP +
Login V2

# How the Problems Are Solved

* `UpdateHumanUserRequest` is added to
`RetrieveIdentityProviderIntentResponse`
* `UpdateHumanUserRequest` is returned in the
`RetrieveIdentityProviderIntentResponse` when the user already exists
during external IDP auth, which is then used in the frontend to update
the user info

# Additional Changes

* Moved integration tests related to user intent to a separate test file
* Fix redirection after external IDP user registration

# Additional Context
- Closes #10838
- Follow up: https://github.com/zitadel/zitadel/issues/11053

---------

Co-authored-by: Max Peintner <peintnerm@gmail.com>
(cherry picked from commit d7e9eddb76)
This commit is contained in:
Gayathri Vijayan
2025-11-10 09:50:36 +01:00
committed by Livio Spring
parent 41543725db
commit 2162f866ff
7 changed files with 1139 additions and 1017 deletions

View File

@@ -4,6 +4,7 @@ import { registerUserAndLinkToIDP } from "@/lib/server/register";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Alert } from "./alert";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
@@ -55,6 +56,7 @@ export function RegisterFormIDPIncomplete({
});
const t = useTranslations("register");
const router = useRouter();
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
@@ -85,7 +87,9 @@ export function RegisterFormIDPIncomplete({
return;
}
// If no error, the function has already handled the redirect
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
}
const { errors } = formState;

View File

@@ -76,6 +76,17 @@ describe("processIDPCallback", () => {
email: "test@example.com",
},
},
updateHumanUser: {
username: "testuser",
profile: {
givenName: "Test",
familyName: "User 1",
displayName: "Test User 1",
},
email: {
email: "test@example.com",
},
},
};
const defaultIdp = {
@@ -257,8 +268,8 @@ describe("processIDPCallback", () => {
serviceUrl: "https://api.example.com",
request: expect.objectContaining({
userId: "user123",
profile: defaultIntent.addHumanUser.profile,
email: defaultIntent.addHumanUser.email,
profile: defaultIntent.updateHumanUser.profile,
email: defaultIntent.updateHumanUser.email,
}),
});
});

View File

@@ -119,7 +119,7 @@ export async function processIDPCallback({
console.log("[IDP Process] Intent retrieved successfully, processing business logic");
const { idpInformation, userId, addHumanUser } = intent;
const { idpInformation, userId, addHumanUser, updateHumanUser } = intent;
if (!idpInformation) {
console.error("[IDP Process] IDP information missing");
@@ -161,15 +161,15 @@ export async function processIDPCallback({
// ============================================
if (userId && !link) {
// Auto-update user if enabled
if (options?.isAutoUpdate && addHumanUser) {
if (options?.isAutoUpdate && updateHumanUser) {
try {
await updateHuman({
serviceUrl,
request: create(UpdateHumanUserRequestSchema, {
userId: userId,
profile: addHumanUser.profile,
email: addHumanUser.email,
phone: addHumanUser.phone,
profile: updateHumanUser.profile,
email: updateHumanUser.email,
phone: updateHumanUser.phone,
}),
});
console.log("[IDP Process] User auto-updated successfully");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -169,7 +169,6 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connec
if err != nil {
return nil, err
}
if idpIntent.UserId == "" {
provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "")
if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) {
return nil, err
@@ -202,7 +201,10 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connec
if err != nil {
return nil, err
}
if idpIntent.UserId == "" {
idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId)
} else {
idpIntent.UpdateHumanUser = idpUserToUpdateHumanUser(intent.UserID, idpUser)
}
return connect.NewResponse(idpIntent), nil
}
@@ -377,3 +379,44 @@ func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserReq
}
return addHumanUser
}
func idpUserToUpdateHumanUser(userID string, idpUser idp.User) *user.UpdateHumanUserRequest {
updateHumanUser := &user.UpdateHumanUserRequest{
UserId: userID,
Profile: &user.SetHumanProfile{
GivenName: idpUser.GetFirstName(),
FamilyName: idpUser.GetLastName(),
},
}
if username := idpUser.GetPreferredUsername(); username != "" {
updateHumanUser.Username = &username
}
if nickName := idpUser.GetNickname(); nickName != "" {
updateHumanUser.Profile.NickName = &nickName
}
if displayName := idpUser.GetDisplayName(); displayName != "" {
updateHumanUser.Profile.DisplayName = &displayName
}
if lang := idpUser.GetPreferredLanguage().String(); lang != "" {
updateHumanUser.Profile.PreferredLanguage = &lang
}
if email := string(idpUser.GetEmail()); email != "" {
updateHumanUser.Email = &user.SetHumanEmail{
Email: email,
Verification: &user.SetHumanEmail_SendCode{},
}
if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified {
updateHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified}
}
}
if phone := string(idpUser.GetPhone()); phone != "" {
updateHumanUser.Phone = &user.SetHumanPhone{
Phone: phone,
Verification: &user.SetHumanPhone_SendCode{},
}
if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified {
updateHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified}
}
}
return updateHumanUser
}

View File

@@ -3093,6 +3093,7 @@ message RetrieveIdentityProviderIntentResponse{
}
];
AddHumanUserRequest add_human_user = 4;
UpdateHumanUserRequest update_human_user = 5;
}
message AddIDPLinkRequest{