diff --git a/cmd/zitadel/system-defaults.yaml b/cmd/zitadel/system-defaults.yaml index 9b94f71bc4..330bac863d 100644 --- a/cmd/zitadel/system-defaults.yaml +++ b/cmd/zitadel/system-defaults.yaml @@ -52,6 +52,7 @@ SystemDefaults: EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY VerificationLifetimes: PasswordCheck: 240h #10d + ExternalLoginCheck: 240h #10d MfaInitSkip: 720h #30d MfaSoftwareCheck: 18h MfaHardwareCheck: 12h diff --git a/internal/admin/repository/eventsourcing/handler/user_external_idps.go b/internal/admin/repository/eventsourcing/handler/user_external_idps.go index 1d64fb5a9f..464b7874b3 100644 --- a/internal/admin/repository/eventsourcing/handler/user_external_idps.go +++ b/internal/admin/repository/eventsourcing/handler/user_external_idps.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/config/systemdefaults" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" @@ -80,16 +81,21 @@ func (m *ExternalIDP) processUser(event *models.Event) (err error) { func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { switch event.Type { case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + configView := new(iam_view_model.IDPConfigView) config := new(iam_model.IDPConfig) - config.AppendEvent(event) - exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if event.Type == iam_es_model.IDPConfigChanged { + configView.AppendEvent(iam_model.IDPProviderTypeSystem, event) + } else { + configView.AppendEvent(iam_model.IDPProviderTypeOrg, event) + } + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(configView.IDPConfigID) if err != nil { return err } if event.AggregateType == iam_es_model.IAMAggregate { - config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.iamEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } else { - config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.orgEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } if err != nil { return err diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index f85414f391..c3301296a6 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -16,13 +16,13 @@ type AuthRequestRepository interface { SaveAuthCode(ctx context.Context, id, code, userAgentID string) error DeleteAuthRequest(ctx context.Context, id string) error CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error - CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.ExternalUser) error + CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.ExternalUser, info *model.BrowserInfo) error SelectUser(ctx context.Context, id, userID, userAgentID string) error SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) error VerifyMfaOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error - LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error - AutoRegisterExternalUser(ctx context.Context, user *user_model.User, externalIDP *user_model.ExternalIDP, member *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error + LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *model.BrowserInfo) error + AutoRegisterExternalUser(ctx context.Context, user *user_model.User, externalIDP *user_model.ExternalIDP, member *org_model.OrgMember, authReqID, userAgentID, resourceOwner string, info *model.BrowserInfo) error ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error GetOrgByPrimaryDomain(primaryDomain string) (*org_model.OrgView, error) } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index c64cd4e687..b350cbf2d2 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -43,10 +43,11 @@ type AuthRequestRepo struct { IdGenerator id.Generator - PasswordCheckLifeTime time.Duration - MfaInitSkippedLifeTime time.Duration - MfaSoftwareCheckLifeTime time.Duration - MfaHardwareCheckLifeTime time.Duration + PasswordCheckLifeTime time.Duration + ExternalLoginCheckLifeTime time.Duration + MfaInitSkippedLifeTime time.Duration + MfaSoftwareCheckLifeTime time.Duration + MfaHardwareCheckLifeTime time.Duration IAMID string } @@ -164,7 +165,7 @@ func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, i return repo.AuthRequests.UpdateAuthRequest(ctx, request) } -func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, externalUser *model.ExternalUser) error { +func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, externalUser *model.ExternalUser, info *model.BrowserInfo) error { request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) if err != nil { return err @@ -176,6 +177,11 @@ func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReq if err != nil { return err } + + err = repo.UserEvents.ExternalLoginChecked(ctx, request.UserID, request.WithCurrentInfo(info)) + if err != nil { + return err + } return repo.AuthRequests.UpdateAuthRequest(ctx, request) } @@ -219,7 +225,7 @@ func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, us return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) } -func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error { +func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *model.BrowserInfo) error { request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) if err != nil { return err @@ -228,6 +234,10 @@ func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, u if err != nil { return err } + err = repo.UserEvents.ExternalLoginChecked(ctx, request.UserID, request.WithCurrentInfo(info)) + if err != nil { + return err + } request.LinkingUsers = nil return repo.AuthRequests.UpdateAuthRequest(ctx, request) } @@ -242,7 +252,7 @@ func (repo *AuthRequestRepo) ResetLinkingUsers(ctx context.Context, authReqID, u return repo.AuthRequests.UpdateAuthRequest(ctx, request) } -func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *user_model.User, externalIDP *user_model.ExternalIDP, orgMember *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error { +func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *user_model.User, externalIDP *user_model.ExternalIDP, orgMember *org_model.OrgMember, authReqID, userAgentID, resourceOwner string, info *model.BrowserInfo) error { request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) if err != nil { return err @@ -277,8 +287,13 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis return err } request.UserID = user.AggregateID + request.UserOrgID = user.ResourceOwner request.SelectedIDPConfigID = externalIDP.IDPConfigID request.LinkingUsers = nil + err = repo.UserEvents.ExternalLoginChecked(ctx, request.UserID, request.WithCurrentInfo(info)) + if err != nil { + return err + } return repo.AuthRequests.UpdateAuthRequest(ctx, request) } @@ -475,7 +490,11 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR return nil, err } - if request.SelectedIDPConfigID == "" || (request.SelectedIDPConfigID != "" && request.LinkingUsers != nil && len(request.LinkingUsers) > 0) { + if (request.SelectedIDPConfigID != "" || userSession.SelectedIDPConfigID != "") && (request.LinkingUsers == nil || len(request.LinkingUsers) == 0) { + if !checkVerificationTime(userSession.ExternalLoginVerification, repo.ExternalLoginCheckLifeTime) { + return append(steps, &model.ExternalLoginStep{}), nil + } + } else if (request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "") || (request.SelectedIDPConfigID != "" && request.LinkingUsers != nil && len(request.LinkingUsers) > 0) { if user.InitRequired { return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil } @@ -643,6 +662,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve es_model.UserDeactivated, es_model.HumanPasswordCheckSucceeded, es_model.HumanPasswordCheckFailed, + es_model.HumanExternalLoginCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckFailed, es_model.HumanSignedOut: @@ -689,15 +709,23 @@ func activeUserByID(ctx context.Context, userViewProvider userViewProvider, user } func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider userEventProvider, userID string) (*user_model.UserView, error) { - user, err := viewProvider.UserByID(userID) - if err != nil { - return nil, err + user, viewErr := viewProvider.UserByID(userID) + if viewErr != nil && !errors.IsNotFound(viewErr) { + return nil, viewErr + } else if user == nil { + user = new(user_view_model.UserView) } events, err := eventProvider.UserEventsByID(ctx, userID, user.Sequence) if err != nil { logging.Log("EVENT-dfg42").WithError(err).Debug("error retrieving new events") return user_view_model.UserToModel(user), nil } + if len(events) == 0 { + if viewErr != nil { + return nil, viewErr + } + return user_view_model.UserToModel(user), viewErr + } userCopy := *user for _, event := range events { if err := userCopy.AppendEvent(event); err != nil { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index a24dce8217..637e397806 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -42,9 +42,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string) ([]*user_view_mod } type mockViewUserSession struct { - PasswordVerification time.Time - MfaSoftwareVerification time.Time - Users []mockUser + ExternalLoginVerification time.Time + PasswordVerification time.Time + MfaSoftwareVerification time.Time + Users []mockUser } type mockUser struct { @@ -54,8 +55,9 @@ type mockUser struct { func (m *mockViewUserSession) UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error) { return &user_view_model.UserSessionView{ - PasswordVerification: m.PasswordVerification, - MfaSoftwareVerification: m.MfaSoftwareVerification, + ExternalLoginVerification: m.ExternalLoginVerification, + PasswordVerification: m.PasswordVerification, + MfaSoftwareVerification: m.MfaSoftwareVerification, }, nil } @@ -157,17 +159,18 @@ func (m *mockViewErrOrg) OrgByPrimaryDomain(string) (*org_view_model.OrgView, er func TestAuthRequestRepo_nextSteps(t *testing.T) { type fields struct { - UserEvents *user_event.UserEventstore - AuthRequests *cache.AuthRequestCache - View *view.View - userSessionViewProvider userSessionViewProvider - userViewProvider userViewProvider - userEventProvider userEventProvider - orgViewProvider orgViewProvider - PasswordCheckLifeTime time.Duration - MfaInitSkippedLifeTime time.Duration - MfaSoftwareCheckLifeTime time.Duration - MfaHardwareCheckLifeTime time.Duration + UserEvents *user_event.UserEventstore + AuthRequests *cache.AuthRequestCache + View *view.View + userSessionViewProvider userSessionViewProvider + userViewProvider userViewProvider + userEventProvider userEventProvider + orgViewProvider orgViewProvider + PasswordCheckLifeTime time.Duration + ExternalLoginCheckLifeTime time.Duration + MfaInitSkippedLifeTime time.Duration + MfaSoftwareCheckLifeTime time.Duration + MfaHardwareCheckLifeTime time.Duration } type args struct { request *model.AuthRequest @@ -391,7 +394,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { nil, }, { - "external user (no password set), callback", + "external user (no external verification), external login step", fields{ userSessionViewProvider: &mockViewUserSession{ MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), @@ -405,6 +408,26 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { MfaSoftwareCheckLifeTime: 18 * time.Hour, }, args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, + []model.NextStep{&model.ExternalLoginStep{}}, + nil, + }, + { + "external user (external verification set), callback", + fields{ + userSessionViewProvider: &mockViewUserSession{ + ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + IsEmailVerified: true, + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + ExternalLoginCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, []model.NextStep{&model.RedirectToCallbackStep{}}, nil, }, @@ -427,16 +450,18 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { "external user (no password check needed), callback", fields{ userSessionViewProvider: &mockViewUserSession{ - MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute), }, userViewProvider: &mockViewUser{ PasswordSet: true, IsEmailVerified: true, MfaMaxSetUp: int32(model.MfaLevelSoftware), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, - MfaSoftwareCheckLifeTime: 18 * time.Hour, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + ExternalLoginCheckLifeTime: 10 * 24 * time.Hour, }, args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, []model.NextStep{&model.RedirectToCallbackStep{}}, @@ -468,17 +493,19 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { "external user, mfa not verified, mfa check step", fields{ userSessionViewProvider: &mockViewUserSession{ - PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute), }, userViewProvider: &mockViewUser{ PasswordSet: true, OTPState: int32(user_model.MfaStateReady), MfaMaxSetUp: int32(model.MfaLevelSoftware), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, - PasswordCheckLifeTime: 10 * 24 * time.Hour, - MfaSoftwareCheckLifeTime: 18 * time.Hour, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + ExternalLoginCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, }, args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, []model.NextStep{&model.MfaVerificationStep{ @@ -645,17 +672,18 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := &AuthRequestRepo{ - UserEvents: tt.fields.UserEvents, - AuthRequests: tt.fields.AuthRequests, - View: tt.fields.View, - UserSessionViewProvider: tt.fields.userSessionViewProvider, - UserViewProvider: tt.fields.userViewProvider, - UserEventProvider: tt.fields.userEventProvider, - OrgViewProvider: tt.fields.orgViewProvider, - PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, - MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, - MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime, - MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime, + UserEvents: tt.fields.UserEvents, + AuthRequests: tt.fields.AuthRequests, + View: tt.fields.View, + UserSessionViewProvider: tt.fields.userSessionViewProvider, + UserViewProvider: tt.fields.userViewProvider, + UserEventProvider: tt.fields.userEventProvider, + OrgViewProvider: tt.fields.orgViewProvider, + PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, + ExternalLoginCheckLifeTime: tt.fields.ExternalLoginCheckLifeTime, + MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime, + MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime, } got, err := repo.nextSteps(context.Background(), tt.args.request, tt.args.checkLoggedIn) if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) { @@ -1024,7 +1052,9 @@ func Test_userByID(t *testing.T) { { "not found, not found error", args{ - viewProvider: &mockViewNoUser{}, + userID: "userID", + viewProvider: &mockViewNoUser{}, + eventProvider: &mockEventUser{}, }, nil, errors.IsNotFound, diff --git a/internal/auth/repository/eventsourcing/handler/user_external_idps.go b/internal/auth/repository/eventsourcing/handler/user_external_idps.go index 44432a1c87..3fac6f177f 100644 --- a/internal/auth/repository/eventsourcing/handler/user_external_idps.go +++ b/internal/auth/repository/eventsourcing/handler/user_external_idps.go @@ -5,17 +5,17 @@ import ( "github.com/caos/logging" "github.com/caos/zitadel/internal/config/systemdefaults" caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/iam/repository/eventsourcing" - org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" - org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" - "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" - "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/eventstore/spooler" iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing" iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" + org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" ) type ExternalIDP struct { @@ -80,16 +80,21 @@ func (m *ExternalIDP) processUser(event *models.Event) (err error) { func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { switch event.Type { case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + configView := new(iam_view_model.IDPConfigView) config := new(iam_model.IDPConfig) - config.AppendEvent(event) - exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if event.Type == iam_es_model.IDPConfigChanged { + configView.AppendEvent(iam_model.IDPProviderTypeSystem, event) + } else { + configView.AppendEvent(iam_model.IDPProviderTypeOrg, event) + } + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(configView.IDPConfigID) if err != nil { return err } if event.AggregateType == iam_es_model.IAMAggregate { - config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.iamEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } else { - config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.orgEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } if err != nil { return err diff --git a/internal/auth/repository/eventsourcing/handler/user_session.go b/internal/auth/repository/eventsourcing/handler/user_session.go index 58ac71cfd7..9554f2d1ae 100644 --- a/internal/auth/repository/eventsourcing/handler/user_session.go +++ b/internal/auth/repository/eventsourcing/handler/user_session.go @@ -45,6 +45,7 @@ func (u *UserSession) Reduce(event *models.Event) (err error) { es_model.SignedOut, es_model.HumanPasswordCheckSucceeded, es_model.HumanPasswordCheckFailed, + es_model.HumanExternalLoginCheckSucceeded, es_model.HumanMFAOTPCheckSucceeded, es_model.HumanMFAOTPCheckFailed, es_model.HumanSignedOut: diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 3da02dcf05..c4217d5458 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -136,23 +136,24 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au View: view, }, eventstore.AuthRequestRepo{ - UserEvents: user, - OrgEvents: org, - PolicyEvents: policy, - AuthRequests: authReq, - View: view, - UserSessionViewProvider: view, - UserViewProvider: view, - UserEventProvider: user, - OrgViewProvider: view, - IDPProviderViewProvider: view, - LoginPolicyViewProvider: view, - IdGenerator: idGenerator, - PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, - MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, - MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration, - MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration, - IAMID: systemDefaults.IamID, + UserEvents: user, + OrgEvents: org, + PolicyEvents: policy, + AuthRequests: authReq, + View: view, + UserSessionViewProvider: view, + UserViewProvider: view, + UserEventProvider: user, + OrgViewProvider: view, + IDPProviderViewProvider: view, + LoginPolicyViewProvider: view, + IdGenerator: idGenerator, + PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, + ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, + MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, + MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration, + MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration, + IAMID: systemDefaults.IamID, }, eventstore.TokenRepo{View: view}, eventstore.KeyRepository{ diff --git a/internal/auth_request/model/next_step.go b/internal/auth_request/model/next_step.go index cbaead8e46..f0866fdcf2 100644 --- a/internal/auth_request/model/next_step.go +++ b/internal/auth_request/model/next_step.go @@ -21,6 +21,7 @@ const ( NextStepChangeUsername NextStepLinkUsers NextStepExternalNotFoundOption + NextStepExternalLogin ) type UserSessionState int32 @@ -71,6 +72,14 @@ func (s *PasswordStep) Type() NextStepType { return NextStepPassword } +type ExternalLoginStep struct { + SelectedIDPConfigID string +} + +func (s *ExternalLoginStep) Type() NextStepType { + return NextStepExternalLogin +} + type ChangePasswordStep struct{} func (s *ChangePasswordStep) Type() NextStepType { diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 296955ee85..ae6697c27b 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -53,10 +53,11 @@ type OTPConfig struct { } type VerificationLifetimes struct { - PasswordCheck types.Duration - MfaInitSkip types.Duration - MfaSoftwareCheck types.Duration - MfaHardwareCheck types.Duration + PasswordCheck types.Duration + ExternalLoginCheck types.Duration + MfaInitSkip types.Duration + MfaSoftwareCheck types.Duration + MfaHardwareCheck types.Duration } type DefaultPolicies struct { diff --git a/internal/management/repository/eventsourcing/handler/user_external_idps.go b/internal/management/repository/eventsourcing/handler/user_external_idps.go index 91e8fa91b1..7d38de9150 100644 --- a/internal/management/repository/eventsourcing/handler/user_external_idps.go +++ b/internal/management/repository/eventsourcing/handler/user_external_idps.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/config/systemdefaults" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" @@ -80,16 +81,21 @@ func (m *ExternalIDP) processUser(event *models.Event) (err error) { func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { switch event.Type { case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + configView := new(iam_view_model.IDPConfigView) config := new(iam_model.IDPConfig) - config.AppendEvent(event) - exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if event.Type == iam_es_model.IDPConfigChanged { + configView.AppendEvent(iam_model.IDPProviderTypeSystem, event) + } else { + configView.AppendEvent(iam_model.IDPProviderTypeOrg, event) + } + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(configView.IDPConfigID) if err != nil { return err } if event.AggregateType == iam_es_model.IAMAggregate { - config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.iamEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } else { - config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + config, err = m.orgEvents.GetIDPConfig(context.Background(), event.AggregateID, configView.IDPConfigID) } if err != nil { return err diff --git a/internal/ui/login/handler/external_login_handler.go b/internal/ui/login/handler/external_login_handler.go index 6b28bc7c2c..d75890d1bd 100644 --- a/internal/ui/login/handler/external_login_handler.go +++ b/internal/ui/login/handler/external_login_handler.go @@ -6,6 +6,7 @@ import ( http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/errors" caos_errors "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/models" iam_model "github.com/caos/zitadel/internal/iam/model" @@ -40,6 +41,15 @@ type externalNotFoundOptionData struct { baseData } +func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, selectedIDPConfigID string) { + for _, idp := range authReq.AllowedExternalIDPs { + if idp.IDPConfigID == selectedIDPConfigID { + l.handleIDP(w, r, authReq, selectedIDPConfigID) + } + } + l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed")) +} + func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { data := new(externalIDPData) authReq, err := l.getAuthRequestAndParseData(r, data) @@ -51,7 +61,11 @@ func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, l.zitadelURL, http.StatusFound) return } - idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) + l.handleIDP(w, r, authReq, data.IDPConfigID) +} + +func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, selectedIDPConfigID string) { + idpConfig, err := l.getIDPConfigByID(r, selectedIDPConfigID) if err != nil { l.renderError(w, r, authReq, err) return @@ -117,7 +131,7 @@ func (l *Login) getRPConfig(w http.ResponseWriter, r *http.Request, authReq *mod func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) { externalUser := l.mapTokenToLoginUser(tokens, idpConfig) - err := l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser) + err := l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser, model.BrowserInfoFromRequest(r)) if err != nil { l.renderExternalNotFoundOption(w, r, authReq, nil) return @@ -196,7 +210,7 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig) - err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, authReq.ID, userAgentID, resourceOwner) + err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, authReq.ID, userAgentID, resourceOwner, model.BrowserInfoFromRequest(r)) if err != nil { l.renderExternalNotFoundOption(w, r, authReq, err) return diff --git a/internal/ui/login/handler/link_users_handler.go b/internal/ui/login/handler/link_users_handler.go index ca238486bb..62346cf4ad 100644 --- a/internal/ui/login/handler/link_users_handler.go +++ b/internal/ui/login/handler/link_users_handler.go @@ -13,7 +13,7 @@ const ( func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.LinkExternalUsers(setContext(r.Context(), authReq.UserOrgID), authReq.ID, userAgentID) + err = l.authRepo.LinkExternalUsers(setContext(r.Context(), authReq.UserOrgID), authReq.ID, userAgentID, model.BrowserInfoFromRequest(r)) l.renderLinkUsersDone(w, r, authReq, err) } diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index 839844cc84..a4c2e30b6d 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -156,6 +156,7 @@ func (l *Login) renderNextStep(w http.ResponseWriter, r *http.Request, authReq * authReq, err := l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID) if err != nil { l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-sio0W", "could not get authreq")) + return } if len(authReq.PossibleSteps) == 0 { l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-9sdp4", "no possible steps")) @@ -208,6 +209,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * l.linkUsers(w, r, authReq, err) case *model.ExternalNotFoundOptionStep: l.renderExternalNotFoundOption(w, r, authReq, err) + case *model.ExternalLoginStep: + l.handleExternalLoginStep(w, r, authReq, step.SelectedIDPConfigID) default: l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible")) } diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index a88ecf6757..81a5bcd0fb 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -225,5 +225,6 @@ Errors: NotActive: Benutzer ist nicht aktiv ExternalIDP: IDPTypeNotImplemented: IDP Typ ist nicht implementiert + NotAllowed: Externer Login Provider ist nicht erlaubt optional: (optional) diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index c21db87b1c..3708f773a5 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -225,6 +225,7 @@ Errors: NotActive: User is not active ExternalIDP: IDPTypeNotImplemented: IDP Type is not implemented + NotAllowed: External Login Provider not allowed optional: (optional) diff --git a/internal/user/model/user_session_view.go b/internal/user/model/user_session_view.go index ce6cfccae2..61308571be 100644 --- a/internal/user/model/user_session_view.go +++ b/internal/user/model/user_session_view.go @@ -17,7 +17,9 @@ type UserSessionView struct { UserName string LoginName string DisplayName string + SelectedIDPConfigID string PasswordVerification time.Time + ExternalLoginVerification time.Time MfaSoftwareVerification time.Time MfaSoftwareVerificationType req_model.MfaType MfaHardwareVerification time.Time diff --git a/internal/user/repository/eventsourcing/eventstore.go b/internal/user/repository/eventsourcing/eventstore.go index d0425de443..d15252775f 100644 --- a/internal/user/repository/eventsourcing/eventstore.go +++ b/internal/user/repository/eventsourcing/eventstore.go @@ -592,6 +592,25 @@ func (es *UserEventstore) SetPassword(ctx context.Context, policy *policy_model. return err } +func (es *UserEventstore) ExternalLoginChecked(ctx context.Context, userID string, authRequest *req_model.AuthRequest) error { + user, err := es.UserByID(ctx, userID) + if err != nil { + return err + } + if user.Human == nil { + return errors.ThrowPreconditionFailed(nil, "EVENT-Gns8i", "Errors.User.NotHuman") + } + repoUser := model.UserFromModel(user) + repoAuthRequest := model.AuthRequestFromModel(authRequest) + agg := ExternalLoginCheckSucceededAggregate(es.AggregateCreator(), repoUser, repoAuthRequest) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + func (es *UserEventstore) ChangeMachine(ctx context.Context, machine *usr_model.Machine) (*usr_model.Machine, error) { user, err := es.UserByID(ctx, machine.AggregateID) if err != nil { diff --git a/internal/user/repository/eventsourcing/model/auth_request.go b/internal/user/repository/eventsourcing/model/auth_request.go index fc3744517e..e66d88257c 100644 --- a/internal/user/repository/eventsourcing/model/auth_request.go +++ b/internal/user/repository/eventsourcing/model/auth_request.go @@ -1,22 +1,28 @@ package model import ( + "encoding/json" + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + es_models "github.com/caos/zitadel/internal/eventstore/models" "net" "github.com/caos/zitadel/internal/auth_request/model" ) type AuthRequest struct { - ID string `json:"id,omitempty"` - UserAgentID string `json:"userAgentID,omitempty"` + ID string `json:"id,omitempty"` + UserAgentID string `json:"userAgentID,omitempty"` + SelectedIDPConfigID string `json:"selectedIDPConfigID,omitempty"` *BrowserInfo } func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest { return &AuthRequest{ - ID: request.ID, - UserAgentID: request.AgentID, - BrowserInfo: BrowserInfoFromModel(request.BrowserInfo), + ID: request.ID, + UserAgentID: request.AgentID, + BrowserInfo: BrowserInfoFromModel(request.BrowserInfo), + SelectedIDPConfigID: request.SelectedIDPConfigID, } } @@ -33,3 +39,11 @@ func BrowserInfoFromModel(info *model.BrowserInfo) *BrowserInfo { RemoteIP: info.RemoteIP, } } + +func (a *AuthRequest) SetData(event *es_models.Event) error { + if err := json.Unmarshal(event.Data, a); err != nil { + logging.Log("EVEN-T5df6").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(err, "MODEL-yGmhh", "could not unmarshal event") + } + return nil +} diff --git a/internal/user/repository/eventsourcing/model/types.go b/internal/user/repository/eventsourcing/model/types.go index 3571538d8f..d637d77595 100644 --- a/internal/user/repository/eventsourcing/model/types.go +++ b/internal/user/repository/eventsourcing/model/types.go @@ -84,6 +84,8 @@ const ( HumanPasswordCheckSucceeded models.EventType = "user.human.password.check.succeeded" HumanPasswordCheckFailed models.EventType = "user.human.password.check.failed" + HumanExternalLoginCheckSucceeded models.EventType = "user.human.externallogin.check.succeeded" + HumanExternalIDPReserved models.EventType = "user.human.externalidp.reserved" HumanExternalIDPReleased models.EventType = "user.human.externalidp.released" diff --git a/internal/user/repository/eventsourcing/user.go b/internal/user/repository/eventsourcing/user.go index ca79d9add5..b8430fcdc2 100644 --- a/internal/user/repository/eventsourcing/user.go +++ b/internal/user/repository/eventsourcing/user.go @@ -445,6 +445,16 @@ func PasswordCodeSentAggregate(aggCreator *es_models.AggregateCreator, user *mod } } +func ExternalLoginCheckSucceededAggregate(aggCreator *es_models.AggregateCreator, user *model.User, check *model.AuthRequest) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + agg, err := UserAggregate(ctx, aggCreator, user) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.HumanExternalLoginCheckSucceeded, check) + } +} + func MachineChangeAggregate(aggCreator *es_models.AggregateCreator, user *model.User, machine *model.Machine) func(ctx context.Context) (*es_models.Aggregate, error) { return func(ctx context.Context) (*es_models.Aggregate, error) { if machine == nil { diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go index 669241af79..c952390512 100644 --- a/internal/user/repository/view/model/user_session.go +++ b/internal/user/repository/view/model/user_session.go @@ -30,7 +30,9 @@ type UserSessionView struct { UserName string `json:"-" gorm:"column:user_name"` LoginName string `json:"-" gorm:"column:login_name"` DisplayName string `json:"-" gorm:"column:user_display_name"` + SelectedIDPConfigID string `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"` PasswordVerification time.Time `json:"-" gorm:"column:password_verification"` + ExternalLoginVerification time.Time `json:"-" gorm:"column:external_login_verification"` MfaSoftwareVerification time.Time `json:"-" gorm:"column:mfa_software_verification"` MfaSoftwareVerificationType int32 `json:"-" gorm:"column:mfa_software_verification_type"` MfaHardwareVerification time.Time `json:"-" gorm:"column:mfa_hardware_verification"` @@ -58,7 +60,9 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView { UserName: userSession.UserName, LoginName: userSession.LoginName, DisplayName: userSession.DisplayName, + SelectedIDPConfigID: userSession.SelectedIDPConfigID, PasswordVerification: userSession.PasswordVerification, + ExternalLoginVerification: userSession.ExternalLoginVerification, MfaSoftwareVerification: userSession.MfaSoftwareVerification, MfaSoftwareVerificationType: req_model.MfaType(userSession.MfaSoftwareVerificationType), MfaHardwareVerification: userSession.MfaHardwareVerification, @@ -83,6 +87,12 @@ func (v *UserSessionView) AppendEvent(event *models.Event) { es_model.HumanPasswordCheckSucceeded: v.PasswordVerification = event.CreationDate v.State = int32(req_model.UserSessionStateActive) + case es_model.HumanExternalLoginCheckSucceeded: + data := new(es_model.AuthRequest) + data.SetData(event) + v.ExternalLoginVerification = event.CreationDate + v.SelectedIDPConfigID = data.SelectedIDPConfigID + v.State = int32(req_model.UserSessionStateActive) case es_model.UserPasswordCheckFailed, es_model.UserPasswordChanged, es_model.HumanPasswordCheckFailed, diff --git a/internal/user/repository/view/usermembership_view.go b/internal/user/repository/view/usermembership_view.go index 9006b35f2b..303529010e 100644 --- a/internal/user/repository/view/usermembership_view.go +++ b/internal/user/repository/view/usermembership_view.go @@ -20,7 +20,7 @@ func UserMembershipByIDs(db *gorm.DB, table, userID, aggregateID, objectID strin query := repository.PrepareGetByQuery(table, userIDQuery, aggregateIDQuery, objectIDQuery, memberTypeQuery) err := query(db, memberships) if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-sj8Sw", "Errors.UserMembership.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "VIEW-5Tsji", "Errors.UserMembership.NotFound") } return memberships, err } diff --git a/migrations/cockroach/V1.16__user_session.sql b/migrations/cockroach/V1.16__user_session.sql new file mode 100644 index 0000000000..913a11561c --- /dev/null +++ b/migrations/cockroach/V1.16__user_session.sql @@ -0,0 +1,2 @@ +ALTER TABLE auth.user_sessions ADD COLUMN external_login_verification TIMESTAMPTZ; +ALTER TABLE auth.user_sessions ADD COLUMN selected_idp_config_id TEXT;