diff --git a/internal/api/grpc/auth/password.go b/internal/api/grpc/auth/password.go index cd0f85ae69..0cbc8d4f61 100644 --- a/internal/api/grpc/auth/password.go +++ b/internal/api/grpc/auth/password.go @@ -11,7 +11,7 @@ import ( func (s *Server) UpdateMyPassword(ctx context.Context, req *auth_pb.UpdateMyPasswordRequest) (*auth_pb.UpdateMyPasswordResponse, error) { ctxData := authz.GetCtxData(ctx) - objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword) + objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword, "") if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/password.go b/internal/api/grpc/user/v2/password.go index c5e23f920e..119806c4d9 100644 --- a/internal/api/grpc/user/v2/password.go +++ b/internal/api/grpc/user/v2/password.go @@ -53,9 +53,9 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) switch v := req.GetVerification().(type) { case *user.SetPasswordRequest_CurrentPassword: - details, err = s.command.ChangePassword(ctx, resourceOwner, req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword()) + details, err = s.command.ChangePassword(ctx, resourceOwner, req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "") case *user.SetPasswordRequest_VerificationCode: - details, err = s.command.SetPasswordWithVerifyCode(ctx, resourceOwner, req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword()) + details, err = s.command.SetPasswordWithVerifyCode(ctx, resourceOwner, req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "") case nil: details, err = s.command.SetPassword(ctx, resourceOwner, req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired()) default: diff --git a/internal/api/ui/login/change_password_handler.go b/internal/api/ui/login/change_password_handler.go index ecbbf41028..89b5ea41ab 100644 --- a/internal/api/ui/login/change_password_handler.go +++ b/internal/api/ui/login/change_password_handler.go @@ -3,6 +3,7 @@ package login import ( "net/http" + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" ) @@ -24,7 +25,8 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - _, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + _, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword, userAgentID) if err != nil { l.renderChangePassword(w, r, authReq, err) return diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index c0af8880a2..f177099703 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -71,7 +72,8 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom if authReq != nil { userOrg = authReq.UserOrgID } - _, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + _, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID) if err != nil { l.renderInitPassword(w, r, authReq, data.UserID, "", err) return diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index f88480a5be..119460a8ae 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -5,6 +5,7 @@ import ( "net/http" "strconv" + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -85,7 +86,8 @@ func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authRe l.renderInitUser(w, r, authReq, data.UserID, data.LoginName, "", data.PasswordSet, err) return } - err = l.command.HumanVerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, userOrgID, data.Code, data.Password, initCodeGenerator) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.command.HumanVerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, userOrgID, data.Code, data.Password, userAgentID, initCodeGenerator) if err != nil { l.renderInitUser(w, r, authReq, data.UserID, data.LoginName, "", data.PasswordSet, err) return diff --git a/internal/command/user_human_init.go b/internal/command/user_human_init.go index 4770332704..af807486d4 100644 --- a/internal/command/user_human_init.go +++ b/internal/command/user_human_init.go @@ -50,7 +50,7 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email d return writeModelToObjectDetails(&existingCode.WriteModel), nil } -func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, password string, initCodeGenerator crypto.Generator) error { +func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, password, userAgentID string, initCodeGenerator crypto.Generator) error { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing") } @@ -80,7 +80,7 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne commands = append(commands, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) } if password != "" { - passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, password, false, false) + passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, password, userAgentID, false, false) if err != nil { return err } diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go index cc9dc21b0a..eeffcf7201 100644 --- a/internal/command/user_human_init_test.go +++ b/internal/command/user_human_init_test.go @@ -300,6 +300,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { code string resourceOwner string password string + userAgentID string secretGenerator crypto.Generator } type res struct { @@ -578,6 +579,83 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { }, }, }, + { + name: "valid code with password and userAgentID, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate)), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + user.NewHumanInitializedCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password", + false, + "userAgent1", + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + password: "password", + userAgentID: "userAgent1", + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -585,7 +663,7 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { eventstore: tt.fields.eventstore, userPasswordHasher: tt.fields.userPasswordHasher, } - err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.secretGenerator) + err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.userAgentID, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 251ffe83fb..5d895ec1b5 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -31,10 +31,10 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password stri if err = c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, userID); err != nil { return nil, err } - return c.setPassword(ctx, wm, password, oneTime) + return c.setPassword(ctx, wm, password, "", oneTime) } -func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password string) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string) (objectDetails *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -58,13 +58,13 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, return nil, err } - return c.setPassword(ctx, wm, password, false) + return c.setPassword(ctx, wm, password, userAgentID, false) } // setEncodedPassword add change event from already encoded password to HumanPasswordWriteModel and return the necessary object details for response -func (c *Commands) setEncodedPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) setEncodedPassword(ctx context.Context, wm *HumanPasswordWriteModel, password, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) { agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner) - command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, changeRequired, true) + command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, userAgentID, changeRequired, true) if err != nil { return nil, err } @@ -76,9 +76,9 @@ func (c *Commands) setEncodedPassword(ctx context.Context, wm *HumanPasswordWrit } // setPassword add change event to HumanPasswordWriteModel and return the necessary object details for response -func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) { agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner) - command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, changeRequired, false) + command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, userAgentID, changeRequired, false) if err != nil { return nil, err } @@ -89,7 +89,7 @@ func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, return writeModelToObjectDetails(&wm.WriteModel), nil } -func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggregate, userState domain.UserState, password string, changeRequired, encoded bool) (_ eventstore.Command, err error) { +func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggregate, userState domain.UserState, password, userAgentID string, changeRequired, encoded bool) (_ eventstore.Command, err error) { if err = c.canUpdatePassword(ctx, password, agg.ResourceOwner, userState); err != nil { return nil, err } @@ -101,13 +101,13 @@ func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggre if err = convertPasswapErr(err); err != nil { return nil, err } - return user.NewHumanPasswordChangedEvent(ctx, agg, encodedPassword, changeRequired, ""), nil + return user.NewHumanPasswordChangedEvent(ctx, agg, encodedPassword, changeRequired, userAgentID), nil } - return user.NewHumanPasswordChangedEvent(ctx, agg, password, changeRequired, ""), nil + return user.NewHumanPasswordChangedEvent(ctx, agg, password, changeRequired, userAgentID), nil } // ChangePassword change password of existing user -func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword string) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -126,7 +126,7 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor if err != nil { return nil, err } - return c.setEncodedPassword(ctx, wm, newPasswordHash, false) + return c.setEncodedPassword(ctx, wm, newPasswordHash, userAgentID, false) } // verifyAndUpdatePassword verify if the old password is correct with the encoded hash and diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index 0bb9e613ae..970033a2f8 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -278,6 +278,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { code string resourceOwner string password string + userAgentID string } type res struct { want *domain.ObjectDetails @@ -496,6 +497,83 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { }, }, }, + { + name: "set password with userAgentID, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPasswordCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password", + false, + "userAgent1", + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + code: "a", + userAgentID: "userAgent1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -504,7 +582,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) { userPasswordHasher: tt.fields.userPasswordHasher, userEncryption: tt.fields.userEncryption, } - got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password) + got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.userAgentID) if tt.res.err == nil { assert.NoError(t, err) } @@ -528,6 +606,7 @@ func TestCommandSide_ChangePassword(t *testing.T) { resourceOwner string oldPassword string newPassword string + userAgentID string } type res struct { want *domain.ObjectDetails @@ -745,6 +824,74 @@ func TestCommandSide_ChangePassword(t *testing.T) { }, }, }, + { + name: "change password with userAgentID, ok", + fields: fields{ + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + oldPassword: "password", + newPassword: "password1", + userAgentID: "userAgent1", + }, + expect: []expect{ + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password", + false, + "")), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "$plain$x$password1", + false, + "userAgent1", + ), + ), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -752,7 +899,7 @@ func TestCommandSide_ChangePassword(t *testing.T) { eventstore: eventstoreExpect(t, tt.expect...), userPasswordHasher: tt.fields.userPasswordHasher, } - got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword) + got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.userAgentID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index c0a61b5a30..524f90a83b 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -394,7 +394,7 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com // password already hashed in request if password.EncodedPasswordHash != nil { - cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.EncodedPasswordHash, password.ChangeRequired, true) + cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.EncodedPasswordHash, "", password.ChangeRequired, true) if cmd != nil { return append(cmds, cmd), err } @@ -402,7 +402,7 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com } // password already hashed in verify if encodedPassword != "" { - cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, encodedPassword, password.ChangeRequired, true) + cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, encodedPassword, "", password.ChangeRequired, true) if cmd != nil { return append(cmds, cmd), err } @@ -410,7 +410,7 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com } // password still to be hashed if password.Password != nil { - cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.Password, password.ChangeRequired, false) + cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.Password, "", password.ChangeRequired, false) if cmd != nil { return append(cmds, cmd), err }