mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-02 12:32:24 +00:00
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:
committed by
Livio Spring
parent
41543725db
commit
2162f866ff
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
1040
internal/api/grpc/user/v2/integration_test/intent_test.go
Normal file
1040
internal/api/grpc/user/v2/integration_test/intent_test.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -169,40 +169,42 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "")
|
||||
if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) {
|
||||
return nil, err
|
||||
}
|
||||
var idpUser idp.User
|
||||
switch p := provider.(type) {
|
||||
case *apple.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, apple.InitUser())
|
||||
case *oauth.Provider:
|
||||
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
|
||||
case *oidc.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
||||
case *jwt.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser())
|
||||
case *azuread.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, p.User())
|
||||
case *github.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{})
|
||||
case *gitlab.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
||||
case *google.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser())
|
||||
case *saml.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{})
|
||||
case *ldap.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{})
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
}
|
||||
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
|
||||
}
|
||||
var idpUser idp.User
|
||||
switch p := provider.(type) {
|
||||
case *apple.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, apple.InitUser())
|
||||
case *oauth.Provider:
|
||||
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
|
||||
case *oidc.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
||||
case *jwt.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser())
|
||||
case *azuread.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, p.User())
|
||||
case *github.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{})
|
||||
case *gitlab.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
||||
case *google.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser())
|
||||
case *saml.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{})
|
||||
case *ldap.Provider:
|
||||
idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{})
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3093,6 +3093,7 @@ message RetrieveIdentityProviderIntentResponse{
|
||||
}
|
||||
];
|
||||
AddHumanUserRequest add_human_user = 4;
|
||||
UpdateHumanUserRequest update_human_user = 5;
|
||||
}
|
||||
|
||||
message AddIDPLinkRequest{
|
||||
|
||||
Reference in New Issue
Block a user