From 3270a94291d51a122b2df1b866e2ff091b9d2f8b Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 18 Oct 2022 16:48:26 +0200 Subject: [PATCH] fix: idp usage (#4571) * fix: send email verification instead of init code for idp users * fix: select single idp of external only users * fix: use single idp on login --- internal/api/grpc/admin/import.go | 7 +- internal/api/grpc/management/user.go | 6 +- .../api/ui/login/external_register_handler.go | 14 +++- internal/api/ui/login/login_handler.go | 8 ++ internal/api/ui/login/register_handler.go | 7 +- .../eventsourcing/eventstore/auth_request.go | 28 ++++++- .../eventstore/auth_request_test.go | 81 +++++++++++++++++-- .../repository/eventsourcing/repository.go | 1 + internal/command/user_human.go | 46 ++++++----- internal/command/user_human_test.go | 4 +- internal/domain/human.go | 5 +- 11 files changed, 172 insertions(+), 35 deletions(-) diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 42b3d25a5c..4d878a144d 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -16,6 +16,7 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/management" @@ -306,6 +307,10 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm if err != nil { return nil, nil, err } + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.userCodeAlg) + if err != nil { + return nil, nil, err + } phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg) if err != nil { return nil, nil, err @@ -521,7 +526,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm logging.Debugf("import user: %s", user.GetUserId()) human, passwordless := management.ImportHumanUserRequestToDomain(user.User) human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, initCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 22f051189a..d0e6e89c9f 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -235,6 +235,10 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.userCodeAlg) + if err != nil { + return nil, err + } phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg) if err != nil { return nil, err @@ -243,7 +247,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, initCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go index 8cd239f58c..751457397f 100644 --- a/internal/api/ui/login/external_register_handler.go +++ b/internal/api/ui/login/external_register_handler.go @@ -155,12 +155,17 @@ func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, aut l.renderRegisterOption(w, r, authReq, err) return } + emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg) if err != nil { l.renderRegisterOption(w, r, authReq, err) return } - _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, nil, initCodeGenerator, phoneCodeGenerator) + _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegisterOption(w, r, authReq, err) return @@ -230,12 +235,17 @@ func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Reque l.renderRegisterOption(w, r, authReq, err) return } + emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg) if err != nil { l.renderRegisterOption(w, r, authReq, err) return } - _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, nil, initCodeGenerator, phoneCodeGenerator) + _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegisterOption(w, r, authReq, err) return diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go index 18cf3cbaf9..6d4e317e3b 100644 --- a/internal/api/ui/login/login_handler.go +++ b/internal/api/ui/login/login_handler.go @@ -95,6 +95,10 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + if singleIDPAllowed(authReq) { + l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) + return + } data := l.getUserData(r, authReq, "Login", errID, errMessage) funcs := map[string]interface{}{ "hasUsernamePasswordLogin": func() bool { @@ -109,3 +113,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom } l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs) } + +func singleIDPAllowed(authReq *domain.AuthRequest) bool { + return authReq != nil && authReq.LoginPolicy != nil && !authReq.LoginPolicy.AllowUsernamePassword && authReq.LoginPolicy.AllowExternalIDP && len(authReq.AllowedExternalIDPs) == 1 +} diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index 6d2d69e3bd..d3e8b21bf8 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -73,12 +73,17 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { l.renderRegister(w, r, authRequest, data, err) return } + emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg) + if err != nil { + l.renderRegister(w, r, authRequest, data, err) + return + } phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg) if err != nil { l.renderRegister(w, r, authRequest, data, err) return } - user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, nil, initCodeGenerator, phoneCodeGenerator) + user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegister(w, r, authRequest, data, err) return diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 88d4e337b1..d1370508cd 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -48,6 +48,7 @@ type AuthRequestRepo struct { LockoutPolicyViewProvider lockoutPolicyViewProvider PrivacyPolicyProvider privacyPolicyProvider IDPProviderViewProvider idpProviderViewProvider + IDPUserLinksProvider idpUserLinksProvider UserGrantProvider userGrantProvider ProjectProvider projectProvider ApplicationProvider applicationProvider @@ -83,6 +84,10 @@ type idpProviderViewProvider interface { IDPProvidersByAggregateIDAndState(string, string, iam_model.IDPConfigState) ([]*iam_view_model.IDPProviderView, error) } +type idpUserLinksProvider interface { + IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery) (*query.IDPUserLinks, error) +} + type userEventProvider interface { UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) } @@ -469,11 +474,15 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis if err != nil { return err } + emailCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, repo.UserCodeAlg) + if err != nil { + return err + } phoneCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, repo.UserCodeAlg) if err != nil { return err } - human, err := repo.Command.RegisterHuman(ctx, resourceOwner, registerUser, externalIDP, orgMemberRoles, initCodeGenerator, phoneCodeGenerator) + human, err := repo.Command.RegisterHuman(ctx, resourceOwner, registerUser, externalIDP, orgMemberRoles, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { return err } @@ -909,11 +918,18 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth } isInternalLogin := request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "" - if !isInternalLogin && len(request.LinkingUsers) == 0 && !checkVerificationTimeMaxAge(userSession.ExternalLoginVerification, request.LoginPolicy.ExternalLoginCheckLifetime, request) { + idps, err := checkExternalIDPsOfUser(ctx, repo.IDPUserLinksProvider, user.ID) + if err != nil { + return nil, err + } + if (!isInternalLogin || len(idps.Links) > 0) && len(request.LinkingUsers) == 0 && !checkVerificationTimeMaxAge(userSession.ExternalLoginVerification, request.LoginPolicy.ExternalLoginCheckLifetime, request) { selectedIDPConfigID := request.SelectedIDPConfigID if selectedIDPConfigID == "" { selectedIDPConfigID = userSession.SelectedIDPConfigID } + if selectedIDPConfigID == "" { + selectedIDPConfigID = idps.Links[0].IDPID + } return append(steps, &domain.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil } if isInternalLogin || (!isInternalLogin && len(request.LinkingUsers) > 0) { @@ -976,6 +992,14 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth return append(steps, &domain.RedirectToCallbackStep{}), nil } +func checkExternalIDPsOfUser(ctx context.Context, idpUserLinksProvider idpUserLinksProvider, userID string) (*query.IDPUserLinks, error) { + userIDQuery, err := query.NewIDPUserLinksUserIDSearchQuery(userID) + if err != nil { + return nil, err + } + return idpUserLinksProvider.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery}}) +} + func (repo *AuthRequestRepo) usersForUserSelection(request *domain.AuthRequest) ([]domain.UserSelection, error) { userSessions, err := userSessionsByUserAgentID(repo.UserSessionViewProvider, request.AgentID, request.InstanceID) if 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 bc6b519d93..e7b8c51b78 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -234,6 +234,14 @@ func (m *mockApp) AppByOIDCClientID(ctx context.Context, id string) (*query.App, return nil, errors.ThrowNotFound(nil, "ERROR", "error") } +type mockIDPUserLinks struct { + idps []*query.IDPUserLink +} + +func (m *mockIDPUserLinks) IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery) (*query.IDPUserLinks, error) { + return &query.IDPUserLinks{Links: m.idps}, nil +} + func TestAuthRequestRepo_nextSteps(t *testing.T) { type fields struct { AuthRequests *cache.AuthRequestCache @@ -247,6 +255,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { applicationProvider applicationProvider loginPolicyProvider loginPolicyViewProvider lockoutPolicyProvider lockoutPolicyViewProvider + idpUserLinksProvider idpUserLinksProvider } type args struct { request *domain.AuthRequest @@ -498,6 +507,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, []domain.NextStep{&domain.PasswordStep{}}, @@ -515,6 +525,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, nil, @@ -535,6 +546,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, []domain.NextStep{&domain.InitUserStep{ @@ -561,6 +573,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { MultiFactorCheckLifetime: 10 * time.Hour, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, []domain.NextStep{&domain.PasswordlessRegistrationPromptStep{}}, @@ -585,6 +598,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { MultiFactorCheckLifetime: 10 * time.Hour, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, []domain.NextStep{&domain.PasswordlessStep{}}, @@ -610,6 +624,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { MultiFactorCheckLifetime: 10 * time.Hour, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, []domain.NextStep{&domain.PasswordlessStep{PasswordSet: true}}, @@ -635,7 +650,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -661,14 +677,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, []domain.NextStep{&domain.InitPasswordStep{}}, nil, }, { - "external user (no external verification), external login step", + "external user (idp selected, no external verification), external login step", fields{ userSessionViewProvider: &mockViewUserSession{ SecondFactorVerification: testNow.Add(-5 * time.Minute), @@ -689,6 +706,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SecondFactorCheckLifetime: 18 * time.Hour, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -699,6 +717,40 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []domain.NextStep{&domain.ExternalLoginStep{SelectedIDPConfigID: "IDPConfigID"}}, nil, }, + { + "external user (only idp available, no external verification), external login step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + SecondFactorVerification: testNow.Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + IsEmailVerified: true, + MFAMaxSetUp: int32(domain.MFALevelSecondFactor), + }, + userEventProvider: &mockEventUser{}, + lockoutPolicyProvider: &mockLockoutPolicy{ + policy: &query.LockoutPolicy{ + ShowFailures: true, + }, + }, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + loginPolicyProvider: &mockLoginPolicy{ + policy: &query.LoginPolicy{ + SecondFactorCheckLifetime: 18 * time.Hour, + }, + }, + idpUserLinksProvider: &mockIDPUserLinks{ + idps: []*query.IDPUserLink{{IDPID: "IDPConfigID"}}, + }, + }, + args{&domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + SecondFactorCheckLifetime: 18 * time.Hour, + }}, false}, + []domain.NextStep{&domain.ExternalLoginStep{SelectedIDPConfigID: "IDPConfigID"}}, + nil, + }, { "external user (external verification set), callback", fields{ @@ -720,6 +772,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -754,6 +807,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { PasswordCheckLifetime: 10 * 24 * time.Hour, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, []domain.NextStep{&domain.PasswordStep{}}, @@ -781,6 +835,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -814,6 +869,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -847,6 +903,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -881,6 +938,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -918,6 +976,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -949,6 +1008,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -980,6 +1040,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1014,6 +1075,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1049,6 +1111,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1085,6 +1148,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1123,6 +1187,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1162,6 +1227,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1200,6 +1266,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1239,6 +1306,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{&domain.AuthRequest{ UserID: "UserID", @@ -1274,8 +1342,9 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SecondFactorCheckLifetime: 18 * time.Hour, }, }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -1306,6 +1375,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + idpUserLinksProvider: &mockIDPUserLinks{}, }, args{ &domain.AuthRequest{ @@ -1336,6 +1406,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ApplicationProvider: tt.fields.applicationProvider, LoginPolicyViewProvider: tt.fields.loginPolicyProvider, LockoutPolicyViewProvider: tt.fields.lockoutPolicyProvider, + IDPUserLinksProvider: tt.fields.idpUserLinksProvider, } got, err := repo.nextSteps(context.Background(), tt.args.request, tt.args.checkLoggedIn) if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) { diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index b21acba79d..4efdd041d0 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -80,6 +80,7 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, command *command.Comma UserCommandProvider: command, UserEventProvider: &userRepo, IDPProviderViewProvider: view, + IDPUserLinksProvider: queries, LockoutPolicyViewProvider: queries, LoginPolicyViewProvider: queries, UserGrantProvider: queryView, diff --git a/internal/command/user_human.go b/internal/command/user_human.go index fa7c985b5f..4003a7cf98 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -272,12 +272,12 @@ func (h *AddHuman) ensureDisplayName() { h.DisplayName = h.FirstName + " " + h.LastName } -//shouldAddInitCode returns true for all added Humans which: +// shouldAddInitCode returns true for all added Humans which: // - were not added from an external IDP // - and either: -// - have no verified email -// and / or -// - have no authentication method (password / passwordless) +// - have no verified email +// and / or +// - have no authentication method (password / passwordless) func (h *AddHuman) shouldAddInitCode() bool { return !h.ExternalIDP && !h.Email.Verified || @@ -285,7 +285,7 @@ func (h *AddHuman) shouldAddInitCode() bool { h.Password == "" } -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { if orgID == "" { return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing") } @@ -309,7 +309,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } @@ -333,7 +333,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) { +func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) { if orgID == "" { return nil, errors.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing") } @@ -352,7 +352,7 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai if !loginPolicy.AllowRegister { return nil, errors.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed") } - userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { return nil, err } @@ -386,21 +386,21 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai return writeModelToHuman(registeredHuman), nil } -func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { if orgID == "" || !human.IsValid() { return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid") } if human.Password != nil && human.Password.SecretString != "" { human.Password.ChangeRequired = true } - return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { if orgID == "" || !human.IsValid() { return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { return nil, nil, nil, "", err } @@ -415,7 +415,7 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain. return events, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { if human == nil { return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-JKefw", "Errors.User.Invalid") } @@ -428,10 +428,10 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai if human.Password != nil && human.Password.SecretString != "" { human.Password.ChangeRequired = false } - return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { if err := human.CheckDomainPolicy(domainPolicy); err != nil { return nil, nil, err } @@ -490,10 +490,16 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. return nil, nil, err } events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry)) - } - - if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified { - events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) + } else { + if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified { + events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) + } else { + emailCode, err := domain.NewEmailCode(emailCodeGenerator) + if err != nil { + return nil, nil, err + } + events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry)) + } } if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { @@ -527,7 +533,7 @@ func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner s return err } -///TODO: adlerhurst maybe we can simplify createAddHumanEvent and createRegisterHumanEvent +// TODO: adlerhurst maybe we can simplify createAddHumanEvent and createRegisterHumanEvent func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, human *domain.Human, userLoginMustBeDomain bool) *user.HumanAddedEvent { addEvent := user.NewHumanAddedEvent( ctx, diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index c19beaac05..9d0e7110d1 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1460,7 +1460,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { idGenerator: tt.fields.idGenerator, userPasswordAlg: tt.fields.userPasswordAlg, } - gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -2487,7 +2487,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { idGenerator: tt.fields.idGenerator, userPasswordAlg: tt.fields.userPasswordAlg, } - got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles, tt.args.secretGenerator, tt.args.secretGenerator) + got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/domain/human.go b/internal/domain/human.go index e83797a12e..75a03bec32 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -89,7 +89,10 @@ func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwor } func (u *Human) IsInitialState(passwordless, externalIDPs bool) bool { - return u.Email == nil || !u.IsEmailVerified || !externalIDPs && !passwordless && (u.Password == nil || u.Password.SecretString == "") && (u.HashedPassword == nil || u.HashedPassword.SecretString == "") + if externalIDPs { + return false + } + return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && (u.HashedPassword == nil || u.HashedPassword.SecretString == "") } func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {