fix: correct unmarshall of EntraID userinfo when retrieving intent information (#10507)

# Which Problems Are Solved

EntraID userinfo gets incorrectly unmarshalled again in the
`RetrieveIdentityProviderIntent` endpoint.

# How the Problems Are Solved

Correctly use the already available information and not try to marshall
it into a `RawInformation` struct again.

# Additional Changes

None

# Additional Context

Closes https://github.com/zitadel/typescript/issues/578

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2025-08-22 07:35:58 +02:00
committed by GitHub
parent 473c33754f
commit 93ea30ba2e
5 changed files with 213 additions and 12 deletions

View File

@@ -2182,6 +2182,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
azureIdpID := Instance.AddAzureADProvider(IamCTX, gofakeit.AppName()).GetId()
oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId()
samlIdpID := Instance.AddSAMLPostProvider(IamCTX)
ldapIdpID := Instance.AddLDAPProvider(IamCTX)
@@ -2208,22 +2209,32 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
require.NoError(t, err)
// make sure the intent is consumed
Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
azureADSuccessful, azureADToken, azureADChangeDate, azureADSequence, err := sink.SuccessfulAzureADIntent(Instance.ID(), azureIdpID, "id", "", expiry)
require.NoError(t, err)
azureADSuccessfulWithUserID, azureADWithUserIDToken, azureADWithUserIDChangeDate, azureADWithUserIDSequence, err := sink.SuccessfulAzureADIntent(Instance.ID(), azureIdpID, "id", "user", expiry)
require.NoError(t, err)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
require.NoError(t, err)
jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry)
require.NoError(t, err)
jwtSuccessfulWithUserID, jwtWithUserToken, jwtWithUserChangeDate, jwtWithUserSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RetrieveIdentityProviderIntentRequest
@@ -2368,6 +2379,105 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: true,
},
{
name: "retrieve successful azure AD intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: azureADSuccessful,
IdpIntentToken: azureADToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(azureADChangeDate),
ResourceOwner: Instance.ID(),
Sequence: azureADSequence,
},
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: azureIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"userPrincipalName": "username",
"displayName": "displayname",
"givenName": "firstname",
"surname": "lastname",
"mail": "email@email.com",
})
require.NoError(t, err)
return s
}(),
},
AddHumanUser: &user.AddHumanUserRequest{
Username: gu.Ptr("username"),
Profile: &user.SetHumanProfile{
PreferredLanguage: gu.Ptr("und"),
GivenName: "firstname",
FamilyName: "lastname",
DisplayName: gu.Ptr("displayname"),
},
IdpLinks: []*user.IDPLink{
{IdpId: azureIdpID, UserId: "id", UserName: "username"},
},
Email: &user.SetHumanEmail{
Email: "email@email.com",
Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}},
},
},
},
wantErr: false,
},
{
name: "retrieve successful azure AD intent with user ID",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: azureADSuccessfulWithUserID,
IdpIntentToken: azureADWithUserIDToken,
},
},
want: &user.RetrieveIdentityProviderIntentResponse{
Details: &object.Details{
ChangeDate: timestamppb.New(azureADWithUserIDChangeDate),
ResourceOwner: Instance.ID(),
Sequence: azureADWithUserIDSequence,
},
UserId: "user",
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Oauth{
Oauth: &user.IDPOAuthAccessInformation{
AccessToken: "accessToken",
IdToken: gu.Ptr("idToken"),
},
},
IdpId: azureIdpID,
UserId: "id",
UserName: "username",
RawInformation: func() *structpb.Struct {
s, err := structpb.NewStruct(map[string]interface{}{
"id": "id",
"userPrincipalName": "username",
"displayName": "displayname",
"givenName": "firstname",
"surname": "lastname",
"mail": "email@email.com",
})
require.NoError(t, err)
return s
}(),
},
},
wantErr: false,
},
{
name: "retrieve successful oidc intent",
args: args{

View File

@@ -185,7 +185,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connec
case *jwt.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser())
case *azuread.Provider:
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
idpUser, err = unmarshalIdpUser(intent.IDPUser, p.User())
case *github.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{})
case *gitlab.Provider:

View File

@@ -161,17 +161,17 @@ func (p *Provider) User() idp.User {
// AzureAD does not return an `email_verified` claim.
// The verification can be automatically activated on the provider ([WithEmailVerified])
type User struct {
ID string `json:"id"`
BusinessPhones []domain.PhoneNumber `json:"businessPhones"`
DisplayName string `json:"displayName"`
FirstName string `json:"givenName"`
JobTitle string `json:"jobTitle"`
Email domain.EmailAddress `json:"mail"`
MobilePhone domain.PhoneNumber `json:"mobilePhone"`
OfficeLocation string `json:"officeLocation"`
PreferredLanguage string `json:"preferredLanguage"`
LastName string `json:"surname"`
UserPrincipalName string `json:"userPrincipalName"`
ID string `json:"id,omitempty"`
BusinessPhones []domain.PhoneNumber `json:"businessPhones,omitempty"`
DisplayName string `json:"displayName,omitempty"`
FirstName string `json:"givenName,omitempty"`
JobTitle string `json:"jobTitle,omitempty"`
Email domain.EmailAddress `json:"mail,omitempty"`
MobilePhone domain.PhoneNumber `json:"mobilePhone,omitempty"`
OfficeLocation string `json:"officeLocation,omitempty"`
PreferredLanguage string `json:"preferredLanguage,omitempty"`
LastName string `json:"surname,omitempty"`
UserPrincipalName string `json:"userPrincipalName,omitempty"`
isEmailVerified bool
}

View File

@@ -632,6 +632,34 @@ func (i *Instance) AddProviderToDefaultLoginPolicy(ctx context.Context, id strin
logging.OnError(err).Panic("add provider to default login policy")
}
func (i *Instance) AddAzureADProvider(ctx context.Context, name string) *admin.AddAzureADProviderResponse {
resp, err := i.Client.Admin.AddAzureADProvider(ctx, &admin.AddAzureADProviderRequest{
Name: name,
ClientId: "clientID",
ClientSecret: "clientSecret",
Tenant: nil,
EmailVerified: false,
Scopes: []string{"openid", "profile", "email"},
ProviderOptions: &idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
},
})
logging.OnError(err).Panic("create Azure AD idp")
mustAwait(func() error {
_, err := i.Client.Admin.GetProviderByID(ctx, &admin.GetProviderByIDRequest{
Id: resp.GetId(),
})
return err
})
return resp
}
func (i *Instance) AddGenericOAuthProvider(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse {
return i.AddGenericOAuthProviderWithOptions(ctx, name, true, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME)
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
@@ -69,6 +70,25 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry t
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
func SuccessfulAzureADIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
Path: successfulIntentAzureADPath(),
}
resp, err := callIntent(u.String(), &SuccessfulIntentRequest{
InstanceID: instanceID,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
}
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
@@ -163,6 +183,7 @@ func StartServer(commands *command.Commands) (close func()) {
router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler)
router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent))
router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent))
router.HandleFunc(successfulIntentAzureADPath(), successfulIntentHandler(commands, createSuccessfulAzureADIntent))
router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent))
router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent))
router.HandleFunc(successfulIntentJWTPath(), successfulIntentHandler(commands, createSuccessfulJWTIntent))
@@ -204,6 +225,10 @@ func successfulIntentOAuthPath() string {
return path.Join(successfulIntentPath(), "/", "oauth")
}
func successfulIntentAzureADPath() string {
return path.Join(successfulIntentPath(), "/", "azuread")
}
func successfulIntentOIDCPath() string {
return path.Join(successfulIntentPath(), "/", "oidc")
}
@@ -423,6 +448,44 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req
}, nil
}
func createSuccessfulAzureADIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
if err != nil {
return nil, err
}
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
if err != nil {
return nil, err
}
idpUser := &azuread.User{
ID: req.IDPUserID,
DisplayName: "displayname",
FirstName: "firstname",
Email: "email@email.com",
LastName: "lastname",
UserPrincipalName: "username",
}
idpSession := &oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
Expiry: req.Expiry,
},
IDToken: "idToken",
},
}
token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID)
if err != nil {
return nil, err
}
return &SuccessfulIntentResponse{
intentID,
token,
writeModel.ChangeDate,
writeModel.ProcessedSequence,
}, nil
}
func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)