diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 76cd067c49..849a456312 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -172,8 +172,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti return nil, err } return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{IdpIntent: &user.IDPIntent{IdpIntentId: intentWriteModel.AggregateID, IdpIntentToken: token}}, + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ + IdpIntent: &user.IDPIntent{ + IdpIntentId: intentWriteModel.AggregateID, + IdpIntentToken: token, + UserId: userID, + }, + }, }, nil } @@ -256,6 +262,7 @@ func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.Encr UserName: intent.IDPUserName, RawInformation: rawInformation, }, + UserId: intent.UserID, } if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index cf69969f33..f037e50803 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -694,7 +694,9 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { idpID := Tester.AddGenericOAuthProvider(t) intentID := Tester.CreateIntent(t, idpID) successfulID, token, changeDate, sequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", "id") + successfulWithUserID, WithUsertoken, WithUserchangeDate, WithUsersequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "user", "id") ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "", "id") + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "user", "id") type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -764,6 +766,44 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulWithUserID, + IdpIntentToken: WithUsertoken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(WithUserchangeDate), + ResourceOwner: Tester.Organisation.ID, + Sequence: WithUsersequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, { name: "retrieve successful ldap intent", args: args{ @@ -809,6 +849,52 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful ldap intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: ldapSuccessfulWithUserID, + IdpIntentToken: ldapWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(ldapWithUserChangeDate), + ResourceOwner: Tester.Organisation.ID, + Sequence: ldapWithUserSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"id"}, + "username": []interface{}{"username"}, + "language": []interface{}{"en"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "preferredUsername": "username", + "preferredLanguage": "en", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go index e454bc5ae3..48fbf7550f 100644 --- a/internal/api/grpc/user/v2/user_test.go +++ b/internal/api/grpc/user/v2/user_test.go @@ -84,9 +84,65 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { resp: nil, err: caos_errs.ThrowInternal(nil, "id", "invalid key id"), }, + }, { + "successful oauth", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPAccessToken: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("accessToken"), + }, + IDPIDToken: "idToken", + UserID: "", + State: domain.IDPIntentStateSucceeded, + }, + alg: decryption(nil), + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + err: nil, + }, }, { - "successful oauth", + "successful oauth with linked user", args{ intent: &command.IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ @@ -138,11 +194,72 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { return s }(), }, + UserId: "userID", }, err: nil, }, }, { "successful ldap", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPEntryAttributes: map[string][]string{ + "id": {"idpUserID"}, + "firstName": {"firstname1", "firstname2"}, + "lastName": {"lastname"}, + }, + UserID: "", + State: domain.IDPIntentStateSucceeded, + }, + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"idpUserID"}, + "firstName": []interface{}{"firstname1", "firstname2"}, + "lastName": []interface{}{"lastname"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + err: nil, + }, + }, { + "successful ldap with linked user", args{ intent: &command.IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ @@ -198,6 +315,7 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { return s }(), }, + UserId: "userID", }, err: nil, }, diff --git a/proto/zitadel/user/v2beta/idp.proto b/proto/zitadel/user/v2beta/idp.proto index c8b5c27ae6..9bf11c7c30 100644 --- a/proto/zitadel/user/v2beta/idp.proto +++ b/proto/zitadel/user/v2beta/idp.proto @@ -57,7 +57,7 @@ message IDPIntent { description: "ID of the IDP intent" min_length: 1; max_length: 200; - example: "\"163840776835432705=\""; + example: "\"163840776835432705\""; } ]; string idp_intent_token = 2 [ @@ -68,6 +68,13 @@ message IDPIntent { example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\""; } ]; + string user_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the ZITADEL user if external user already linked" + max_length: 200; + example: "\"163840776835432345\""; + } + ]; } message IDPInformation{ diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 5fcd72eb91..94dcc9b3fd 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -1159,6 +1159,12 @@ message RetrieveIdentityProviderIntentRequest{ message RetrieveIdentityProviderIntentResponse{ zitadel.object.v2beta.Details details = 1; IDPInformation idp_information = 2; + string user_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the user in ZITADEL if external user is linked" + example: "\"163840776835432345\""; + } + ]; } message AddIDPLinkRequest{