diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index d87be02b7fc..80ff8e43941 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -2206,6 +2206,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) @@ -2232,22 +2233,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 @@ -2392,6 +2403,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{ diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index c26adba24d1..5bb2f50a95f 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -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: diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index a15f793e375..3e2594b7c1d 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -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 } diff --git a/internal/integration/client.go b/internal/integration/client.go index 2e87b0fd5f9..6d7737ff1c0 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -611,6 +611,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) } diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 653c5236d6d..13d735faba4 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -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)