VYPR
High severityNVD Advisory· Published Oct 29, 2025· Updated Oct 30, 2025

Zitadel allows brute-forcing authentication factors

CVE-2025-64102

Description

Zitadel is open-source identity infrastructure software. Prior to 4.6.0, 3.4.3, and 2.71.18, an attacker can perform an online brute-force attack on OTP, TOTP, and passwords. While Zitadel allows preventing online brute force attacks in scenarios like TOTP, Email OTP, or passwords using a lockout mechanism. The mechanism is not enabled by default and can cause a denial of service for the corresponding user if enabled. Additionally, the mitigation strategies were not fully implemented in the more recent resource-based APIs. This vulnerability is fixed in 4.6.0, 3.4.3, and 2.71.18.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/zitadel/zitadel/v2Go
< 2.71.182.71.18
github.com/zitadel/zitadelGo
< 1.80.0-v2.20.0.20251029090735-b8db8cdf9cc81.80.0-v2.20.0.20251029090735-b8db8cdf9cc8

Affected products

1

Patches

1
b8db8cdf9cc8

Merge commit from fork

https://github.com/zitadel/zitadelLivio SpringOct 29, 2025via ghsa
17 files changed · +620 50
  • cmd/defaults.yaml+9 0 modified
    @@ -740,6 +740,15 @@ SystemDefaults:
           # If this is empty, the issuer is the requested domain
           # This is helpful in scenarios with multiple ZITADEL environments or virtual instances
           Issuer: "ZITADEL" # ZITADEL_SYSTEMDEFAULTS_MULTIFACTORS_OTP_ISSUER
    +  Tarpit:
    +    # The amount of failed attempts, the tarpit should start.
    +    MinFailedAttempts: 5 # ZITADEL_SYSTEMDEFAULTS_TARPIT_MINFAILEDATTEMPTS
    +    # The seconds that will be added per step.
    +    StepDuration: 1s # ZITADEL_SYSTEMDEFAULTS_TARPIT_STEPDURATION
    +    # The failed attempts that are needed to increase the tarpit by one step.
    +    StepSize: 5 # ZITADEL_SYSTEMDEFAULTS_TARPIT_STEPSIZE
    +    # The maximum duration the tarpit can reach.
    +    MaxDuration: 10s # ZITADEL_SYSTEMDEFAULTS_TARPIT_MAXDURATION
       DomainVerification:
         VerificationGenerator:
           Length: 32 # ZITADEL_SYSTEMDEFAULTS_DOMAINVERIFICATION_VERIFICATIONGENERATOR_LENGTH
    
  • internal/command/command.go+2 0 modified
    @@ -70,6 +70,7 @@ type Commands struct {
     	defaultRefreshTokenLifetime     time.Duration
     	defaultRefreshTokenIdleLifetime time.Duration
     	phoneCodeVerifier               func(ctx context.Context, id string) (senders.CodeGenerator, error)
    +	tarpit                          func(failedAttempts uint64)
     
     	multifactors            domain.MultifactorConfigs
     	webauthnConfig          *webauthn_helper.Config
    @@ -214,6 +215,7 @@ func StartCommands(
     		repo.newHashedSecret = newHashedSecretWithDefault(secretHasher, defaultSecretGenerators.ClientSecret)
     	}
     	repo.phoneCodeVerifier = repo.phoneCodeVerifierFromConfig
    +	repo.tarpit = defaults.Tarpit.Tarpit()
     	return repo, nil
     }
     
    
  • internal/command/session.go+4 1 modified
    @@ -43,6 +43,7 @@ type SessionCommands struct {
     	getCodeVerifier      func(ctx context.Context, id string) (senders.CodeGenerator, error)
     	now                  func() time.Time
     	maxIdPIntentLifetime time.Duration
    +	tarpit               func(failedAttempts uint64)
     }
     
     func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
    @@ -60,6 +61,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
     		getCodeVerifier:      c.phoneCodeVerifierFromConfig,
     		now:                  time.Now,
     		maxIdPIntentLifetime: c.maxIdPIntentLifetime,
    +		tarpit:               c.tarpit,
     	}
     }
     
    @@ -76,7 +78,7 @@ func CheckUser(id string, resourceOwner string, preferredLanguage *language.Tag)
     // CheckPassword defines a password check to be executed for a session update
     func CheckPassword(password string) SessionCommand {
     	return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
    -		commands, err := checkPassword(ctx, cmd.sessionWriteModel.UserID, password, cmd.eventstore, cmd.hasher, nil)
    +		commands, err := checkPassword(ctx, cmd.sessionWriteModel.UserID, password, cmd.eventstore, cmd.hasher, nil, cmd.tarpit)
     		if err != nil {
     			return commands, err
     		}
    @@ -135,6 +137,7 @@ func CheckTOTP(code string) SessionCommand {
     			cmd.eventstore.FilterToQueryReducer,
     			cmd.totpAlg,
     			nil,
    +			cmd.tarpit,
     		)
     		if err != nil {
     			return commands, err
    
  • internal/command/session_otp.go+2 0 modified
    @@ -143,6 +143,7 @@ func CheckOTPSMS(code string) SessionCommand {
     			cmd.getCodeVerifier,
     			succeededEvent,
     			failedEvent,
    +			cmd.tarpit,
     		)
     		if err != nil {
     			return commands, err
    @@ -183,6 +184,7 @@ func CheckOTPEmail(code string) SessionCommand {
     			nil, // email currently always uses local code checks
     			succeededEvent,
     			failedEvent,
    +			cmd.tarpit,
     		)
     		if err != nil {
     			return commands, err
    
  • internal/command/session_otp_test.go+39 0 modified
    @@ -1037,6 +1037,9 @@ func TestCheckOTPSMS(t *testing.T) {
     				now: func() time.Time {
     					return testNow
     				},
    +				tarpit: func(failedAttempts uint64) {
    +
    +				},
     			}
     
     			gotCmds, err := cmd(context.Background(), cmds)
    @@ -1053,6 +1056,7 @@ func TestCheckOTPEmail(t *testing.T) {
     		userID           string
     		otpCodeChallenge *OTPCode
     		otpAlg           crypto.EncryptionAlgorithm
    +		tarpit           Tarpit
     	}
     	type args struct {
     		code string
    @@ -1073,6 +1077,7 @@ func TestCheckOTPEmail(t *testing.T) {
     			fields: fields{
     				eventstore: expectEventstore(),
     				userID:     "",
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				code: "code",
    @@ -1086,6 +1091,7 @@ func TestCheckOTPEmail(t *testing.T) {
     			fields: fields{
     				eventstore: expectEventstore(),
     				userID:     "userID",
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{},
     			res: res{
    @@ -1099,6 +1105,7 @@ func TestCheckOTPEmail(t *testing.T) {
     					expectFilter(),
     				),
     				userID: "userID",
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				code: "code",
    @@ -1117,6 +1124,7 @@ func TestCheckOTPEmail(t *testing.T) {
     				),
     				userID:           "userID",
     				otpCodeChallenge: nil,
    +				tarpit:           expectTarpit(0),
     			},
     			args: args{
     				code: "code",
    @@ -1153,6 +1161,7 @@ func TestCheckOTPEmail(t *testing.T) {
     					CreationDate: testNow.Add(-10 * time.Minute),
     				},
     				otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit: expectTarpit(1),
     			},
     			args: args{
     				code: "code",
    @@ -1192,6 +1201,7 @@ func TestCheckOTPEmail(t *testing.T) {
     					CreationDate: testNow.Add(-10 * time.Minute),
     				},
     				otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit: expectTarpit(1),
     			},
     			args: args{
     				code: "code",
    @@ -1225,6 +1235,7 @@ func TestCheckOTPEmail(t *testing.T) {
     					CreationDate: testNow,
     				},
     				otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				code: "code",
    @@ -1261,6 +1272,7 @@ func TestCheckOTPEmail(t *testing.T) {
     					CreationDate: testNow,
     				},
     				otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				code: "code",
    @@ -1289,12 +1301,39 @@ func TestCheckOTPEmail(t *testing.T) {
     				now: func() time.Time {
     					return testNow
     				},
    +				tarpit: tt.fields.tarpit.tarpit,
     			}
     
     			gotCmds, err := cmd(context.Background(), cmds)
     			assert.ErrorIs(t, err, tt.res.err)
     			assert.Equal(t, tt.res.errorCommands, gotCmds)
     			assert.Equal(t, tt.res.commands, cmds.eventCommands)
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    +
    +func expectTarpit(requiredFailedAttempts uint64) Tarpit {
    +	return &mockTarpit{
    +		requiredFailedAttempts: requiredFailedAttempts,
    +	}
    +}
    +
    +type mockTarpit struct {
    +	requiredFailedAttempts uint64
    +	failedAttempts         uint64
    +}
    +
    +func (m *mockTarpit) tarpit(failedAttempts uint64) {
    +	m.failedAttempts = failedAttempts
    +}
    +
    +func (m *mockTarpit) metExpectedCalls(t *testing.T) bool {
    +	t.Helper()
    +	return assert.Equalf(t, m.requiredFailedAttempts, m.failedAttempts, "tarpit was called with %d failed attempts, but %d were expected", m.failedAttempts, m.requiredFailedAttempts)
    +}
    +
    +type Tarpit interface {
    +	tarpit(failedAttempts uint64)
    +	metExpectedCalls(t *testing.T) bool
    +}
    
  • internal/command/session_test.go+10 0 modified
    @@ -1091,6 +1091,7 @@ func TestCheckTOTP(t *testing.T) {
     	type fields struct {
     		sessionWriteModel *SessionWriteModel
     		eventstore        func(*testing.T) *eventstore.Eventstore
    +		tarpit            Tarpit
     	}
     
     	tests := []struct {
    @@ -1109,6 +1110,7 @@ func TestCheckTOTP(t *testing.T) {
     					aggregate: sessAgg,
     				},
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
     		},
    @@ -1124,6 +1126,7 @@ func TestCheckTOTP(t *testing.T) {
     				eventstore: expectEventstore(
     					expectFilterError(io.ErrClosedPipe),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			wantErr: io.ErrClosedPipe,
     		},
    @@ -1143,6 +1146,7 @@ func TestCheckTOTP(t *testing.T) {
     						),
     					),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady"),
     		},
    @@ -1169,6 +1173,7 @@ func TestCheckTOTP(t *testing.T) {
     						eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 0, 0, false)),
     					),
     				),
    +				tarpit: expectTarpit(1),
     			},
     			wantErrorCommands: []eventstore.Command{
     				user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
    @@ -1198,6 +1203,7 @@ func TestCheckTOTP(t *testing.T) {
     						eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 1, 1, false)),
     					),
     				),
    +				tarpit: expectTarpit(1),
     			},
     			wantErrorCommands: []eventstore.Command{
     				user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
    @@ -1225,6 +1231,7 @@ func TestCheckTOTP(t *testing.T) {
     					),
     					expectFilter(), // recheck
     				),
    +				tarpit: expectTarpit(0),
     			},
     			wantEventCommands: []eventstore.Command{
     				user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, nil),
    @@ -1253,6 +1260,7 @@ func TestCheckTOTP(t *testing.T) {
     						user.NewUserLockedEvent(ctx, userAgg),
     					),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked"),
     		},
    @@ -1264,11 +1272,13 @@ func TestCheckTOTP(t *testing.T) {
     				eventstore:        tt.fields.eventstore(t),
     				totpAlg:           cryptoAlg,
     				now:               func() time.Time { return testNow },
    +				tarpit:            tt.fields.tarpit.tarpit,
     			}
     			gotCmds, err := CheckTOTP(tt.code)(ctx, cmd)
     			require.ErrorIs(t, err, tt.wantErr)
     			assert.Equal(t, tt.wantErrorCommands, gotCmds)
     			assert.Equal(t, tt.wantEventCommands, cmd.eventCommands)
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    
  • internal/command/user_human_otp.go+7 0 modified
    @@ -170,6 +170,7 @@ func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resource
     		c.eventstore.FilterToQueryReducer,
     		c.multifactors.OTP.CryptoMFA,
     		authRequestDomainToAuthRequestInfo(authRequest),
    +		c.tarpit,
     	)
     
     	_, pushErr := c.eventstore.Push(ctx, commands...)
    @@ -183,6 +184,7 @@ func checkTOTP(
     	queryReducer func(ctx context.Context, r eventstore.QueryReducer) error,
     	alg crypto.EncryptionAlgorithm,
     	optionalAuthRequestInfo *user.AuthRequestInfo,
    +	tarpit func(failedAttempts uint64),
     ) ([]eventstore.Command, error) {
     	if userID == "" {
     		return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
    @@ -222,6 +224,7 @@ func checkTOTP(
     	if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount+1 >= lockoutPolicy.MaxOTPAttempts {
     		commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
     	}
    +	tarpit(existingOTP.CheckFailedCount + 1)
     	return commands, verifyErr
     }
     
    @@ -374,6 +377,7 @@ func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceO
     		c.phoneCodeVerifier,
     		succeededEvent,
     		failedEvent,
    +		c.tarpit,
     	)
     	if len(commands) > 0 {
     		_, pushErr := c.eventstore.Push(ctx, commands...)
    @@ -508,6 +512,7 @@ func (c *Commands) HumanCheckOTPEmail(ctx context.Context, userID, code, resourc
     		nil, // email currently always uses local code checks
     		succeededEvent,
     		failedEvent,
    +		c.tarpit,
     	)
     	if len(commands) > 0 {
     		_, pushErr := c.eventstore.Push(ctx, commands...)
    @@ -576,6 +581,7 @@ func checkOTP(
     	alg crypto.EncryptionAlgorithm,
     	getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error),
     	checkSucceededEvent, checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
    +	tarpit func(failedAttempts uint64),
     ) ([]eventstore.Command, error) {
     	if userID == "" {
     		return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing")
    @@ -627,6 +633,7 @@ func checkOTP(
     	if lockoutPolicy != nil && lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount()+1 >= lockoutPolicy.MaxOTPAttempts {
     		commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
     	}
    +	tarpit(existingOTP.CheckFailedCount() + 1)
     	return commands, verifyErr
     }
     
    
  • internal/command/user_human_otp_test.go+23 0 modified
    @@ -1940,6 +1940,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     		eventstore        func(*testing.T) *eventstore.Eventstore
     		userEncryption    crypto.EncryptionAlgorithm
     		phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
    +		tarpit            Tarpit
     	}
     	type (
     		args struct {
    @@ -1964,6 +1965,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     			name: "userid missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -1979,6 +1981,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     			name: "code missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -1996,6 +1999,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     				eventstore: expectEventstore(
     					expectFilter(),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2019,6 +2023,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     						),
     					),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2088,6 +2093,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(1),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2169,6 +2175,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(1),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2239,6 +2246,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2301,6 +2309,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2380,6 +2389,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     					sender.EXPECT().VerifyCode("verificationID", "code").Return(nil)
     					return sender, nil
     				},
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -2409,9 +2419,11 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
     				eventstore:        tt.fields.eventstore(t),
     				userEncryption:    tt.fields.userEncryption,
     				phoneCodeVerifier: tt.fields.phoneCodeVerifier,
    +				tarpit:            tt.fields.tarpit.tarpit,
     			}
     			err := r.HumanCheckOTPSMS(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.authRequest)
     			assert.ErrorIs(t, err, tt.res.err)
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    @@ -3115,6 +3127,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     	type fields struct {
     		eventstore     func(*testing.T) *eventstore.Eventstore
     		userEncryption crypto.EncryptionAlgorithm
    +		tarpit         Tarpit
     	}
     	type (
     		args struct {
    @@ -3139,6 +3152,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     			name: "userid missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3154,6 +3168,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     			name: "code missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3171,6 +3186,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     				eventstore: expectEventstore(
     					expectFilter(),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3194,6 +3210,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     						),
     					),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3262,6 +3279,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(1),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3342,6 +3360,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(1),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3411,6 +3430,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3472,6 +3492,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     					),
     				),
     				userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
    +				tarpit:         expectTarpit(0),
     			},
     			args: args{
     				ctx:           ctx,
    @@ -3498,9 +3519,11 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
     			r := &Commands{
     				eventstore:     tt.fields.eventstore(t),
     				userEncryption: tt.fields.userEncryption,
    +				tarpit:         tt.fields.tarpit.tarpit,
     			}
     			err := r.HumanCheckOTPEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.authRequest)
     			assert.ErrorIs(t, err, tt.res.err)
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    
  • internal/command/user_human_password.go+84 35 modified
    @@ -103,7 +103,7 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
     		"",
     		userAgentID,
     		changeRequired,
    -		c.checkCurrentPassword(newPassword, "", oldPassword, wm.EncodedHash),
    +		c.checkCurrentPassword(newPassword, "", oldPassword, wm, c.tarpit),
     	)
     }
     
    @@ -140,23 +140,49 @@ func (c *Commands) setPasswordWithVerifyCode(
     	}
     }
     
    +type HumanPasswordCheckWriteModel interface {
    +	GetUserState() domain.UserState
    +	GetPasswordCheckFailedCount() uint64
    +	GetEncodedHash() string
    +	GetResourceOwner() string
    +	GetWriteModel() *eventstore.WriteModel
    +	eventstore.QueryReducer
    +}
    +
     // checkCurrentPassword returns a password check as [setPasswordVerification] implementation
     func (c *Commands) checkCurrentPassword(
    -	newPassword, newEncodedPassword, currentPassword, currentEncodePassword string,
    +	newPassword, newEncodedPassword, currentPassword string,
    +	wm HumanPasswordCheckWriteModel,
    +	tarpit func(failedAttempts uint64),
     ) setPasswordVerification {
    -	// in case the new password is already encoded, we only need to verify the current
    -	if newEncodedPassword != "" {
    -		return func(ctx context.Context) (_ string, err error) {
    -			_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
    -			_, err = c.userPasswordHasher.Verify(currentEncodePassword, currentPassword)
    -			spanPasswap.EndWithError(err)
    -			return "", convertPasswapErr(err)
    +	return func(ctx context.Context) (_ string, err error) {
    +		verify := func(hash, password string) (string, error) {
    +			// in case the new password is already encoded, we only need to verify the current
    +			if newEncodedPassword != "" {
    +				_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
    +				_, err = c.userPasswordHasher.Verify(hash, password)
    +				spanPasswap.EndWithError(err)
    +				return "", convertPasswapErr(err)
    +			}
    +			// otherwise, let's directly verify and return the new generated hash, so we can reuse it in the event
    +			return c.verifyAndUpdatePassword(ctx, hash, password, newPassword)
     		}
    -	}
    -
    -	// otherwise let's directly verify and return the new generate hash, so we can reuse it in the event
    -	return func(ctx context.Context) (string, error) {
    -		return c.verifyAndUpdatePassword(ctx, currentEncodePassword, currentPassword, newPassword)
    +		commands, updated, err := verifyPasswordWithLockoutPolicy(ctx, wm, currentPassword, c.eventstore, verify, nil, tarpit)
    +		// The verification was successful, and we might have an updated hash.
    +		if err == nil {
    +			return updated, nil
    +		}
    +		// If we get here, the verification failed, either due to a precondition (e.g. user not found or locked)
    +		// or due to a wrong password.
    +		// If the former, we just return the error.
    +		if len(commands) == 0 {
    +			return "", err
    +		}
    +		// If the latter, there's at least a failed password check event to push or additionally a lock event.
    +		// We push these events, but return the original error (which might contain details about the failed attempts).
    +		_, pushErr := c.eventstore.Push(ctx, commands...)
    +		logging.OnError(pushErr).Error("error create password check failed event")
    +		return "", err
     	}
     }
     
    @@ -344,7 +370,11 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
     	if !loginPolicy.AllowUsernamePassword {
     		return zerrors.ThrowPreconditionFailed(err, "COMMAND-Dft32", "Errors.Org.LoginPolicy.UsernamePasswordNotAllowed")
     	}
    -	commands, err := checkPassword(ctx, userID, password, c.eventstore, c.userPasswordHasher, authRequestDomainToAuthRequestInfo(authRequest))
    +	var tarpit func(failedAttempts uint64)
    +	if !loginPolicy.IgnoreUnknownUsernames {
    +		tarpit = c.tarpit
    +	}
    +	commands, err := checkPassword(ctx, userID, password, c.eventstore, c.userPasswordHasher, authRequestDomainToAuthRequestInfo(authRequest), tarpit)
     	if len(commands) == 0 {
     		return err
     	}
    @@ -353,7 +383,7 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
     	return err
     }
     
    -func checkPassword(ctx context.Context, userID, password string, es *eventstore.Eventstore, hasher *crypto.Hasher, optionalAuthRequestInfo *user.AuthRequestInfo) ([]eventstore.Command, error) {
    +func checkPassword(ctx context.Context, userID, password string, es *eventstore.Eventstore, hasher *crypto.Hasher, optionalAuthRequestInfo *user.AuthRequestInfo, tarpit func(failedAttempts uint64)) ([]eventstore.Command, error) {
     	if userID == "" {
     		return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing")
     	}
    @@ -362,54 +392,73 @@ func checkPassword(ctx context.Context, userID, password string, es *eventstore.
     	if err != nil {
     		return nil, err
     	}
    -	if !wm.UserState.Exists() {
    -		return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
    -	}
    -	if wm.UserState == domain.UserStateLocked {
    +	commands, _, err := verifyPasswordWithLockoutPolicy(ctx, wm, password, es, hasher.Verify, optionalAuthRequestInfo, tarpit)
    +	return commands, err
    +}
    +
    +func verifyPasswordWithLockoutPolicy(
    +	ctx context.Context,
    +	wm HumanPasswordCheckWriteModel,
    +	password string,
    +	es *eventstore.Eventstore,
    +	verify func(hash string, password string) (newHash string, err error),
    +	optionalAuthRequestInfo *user.AuthRequestInfo,
    +	tarpit func(failedAttempts uint64),
    +) ([]eventstore.Command, string, error) {
    +	if !wm.GetUserState().Exists() {
    +		return nil, "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
    +	}
    +	if wm.GetUserState() == domain.UserStateLocked {
     		wrongPasswordError := &commandErrors.WrongPasswordError{
    -			FailedAttempts: int32(wm.PasswordCheckFailedCount),
    +			FailedAttempts: int32(wm.GetPasswordCheckFailedCount()),
     		}
    -		return nil, zerrors.ThrowPreconditionFailed(wrongPasswordError, "COMMAND-JLK35", "Errors.User.Locked")
    +		return nil, "", zerrors.ThrowPreconditionFailed(wrongPasswordError, "COMMAND-JLK35", "Errors.User.Locked")
     	}
    -	if wm.EncodedHash == "" {
    -		return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")
    +	if wm.GetEncodedHash() == "" {
    +		return nil, "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")
     	}
     
    -	userAgg := UserAggregateFromWriteModel(&wm.WriteModel)
    +	userAgg := UserAggregateFromWriteModel(wm.GetWriteModel())
     	ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
    -	updated, err := hasher.Verify(wm.EncodedHash, password)
    +	updated, err := verify(wm.GetEncodedHash(), password)
     	spanPasswordComparison.EndWithError(err)
    -	err = convertLoginPasswapErr(wm.PasswordCheckFailedCount+1, err)
    +	err = convertLoginPasswapErr(wm.GetPasswordCheckFailedCount()+1, err)
     	commands := make([]eventstore.Command, 0, 2)
     
     	// recheck for additional events (failed password checks or locks)
     	recheckErr := es.FilterToQueryReducer(ctx, wm)
     	if recheckErr != nil {
    -		return nil, recheckErr
    +		return nil, "", recheckErr
     	}
    -	if wm.UserState == domain.UserStateLocked {
    +	if wm.GetUserState() == domain.UserStateLocked {
     		wrongPasswordError := &commandErrors.WrongPasswordError{
    -			FailedAttempts: int32(wm.PasswordCheckFailedCount),
    +			FailedAttempts: int32(wm.GetPasswordCheckFailedCount()),
     		}
    -		return nil, zerrors.ThrowPreconditionFailed(wrongPasswordError, "COMMAND-SFA3t", "Errors.User.Locked")
    +		return nil, "", zerrors.ThrowPreconditionFailed(wrongPasswordError, "COMMAND-SFA3t", "Errors.User.Locked")
     	}
     
     	if err == nil {
     		commands = append(commands, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, optionalAuthRequestInfo))
     		if updated != "" {
     			commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated))
     		}
    -		return commands, nil
    +		return commands, updated, nil
     	}
     
     	commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, optionalAuthRequestInfo))
     
    -	lockoutPolicy, lockoutErr := getLockoutPolicy(ctx, wm.ResourceOwner, es.FilterToQueryReducer)
    +	lockoutPolicy, lockoutErr := getLockoutPolicy(ctx, wm.GetResourceOwner(), es.FilterToQueryReducer)
     	logging.OnError(lockoutErr).Error("unable to get lockout policy")
    -	if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 && wm.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
    +	if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 && wm.GetPasswordCheckFailedCount()+1 >= lockoutPolicy.MaxPasswordAttempts {
     		commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
     	}
    -	return commands, err
    +	// in case the login policy ignores unknown usernames,
    +	// we do not slow down the response time with a tarpit
    +	// since this would leak the user existence
    +	if tarpit != nil {
    +		tarpit(wm.GetPasswordCheckFailedCount() + 1)
    +	}
    +	return commands, "", err
     }
     
     func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {
    
  • internal/command/user_human_password_model.go+20 0 modified
    @@ -25,6 +25,26 @@ type HumanPasswordWriteModel struct {
     	UserState domain.UserState
     }
     
    +func (wm *HumanPasswordWriteModel) GetUserState() domain.UserState {
    +	return wm.UserState
    +}
    +
    +func (wm *HumanPasswordWriteModel) GetPasswordCheckFailedCount() uint64 {
    +	return wm.PasswordCheckFailedCount
    +}
    +
    +func (wm *HumanPasswordWriteModel) GetEncodedHash() string {
    +	return wm.EncodedHash
    +}
    +
    +func (wm *HumanPasswordWriteModel) GetResourceOwner() string {
    +	return wm.ResourceOwner
    +}
    +
    +func (wm *HumanPasswordWriteModel) GetWriteModel() *eventstore.WriteModel {
    +	return &wm.WriteModel
    +}
    +
     func NewHumanPasswordWriteModel(userID, resourceOwner string) *HumanPasswordWriteModel {
     	return &HumanPasswordWriteModel{
     		WriteModel: eventstore.WriteModel{
    
  • internal/command/user_human_password_test.go+221 9 modified
    @@ -760,6 +760,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
     func TestCommandSide_ChangePassword(t *testing.T) {
     	type fields struct {
     		userPasswordHasher *crypto.Hasher
    +		tarpit             Tarpit
     	}
     	type args struct {
     		ctx            context.Context
    @@ -782,8 +783,10 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     		res    res
     	}{
     		{
    -			name:   "userid missing, invalid argument error",
    -			fields: fields{},
    +			name: "userid missing, invalid argument error",
    +			fields: fields{
    +				tarpit: expectTarpit(0),
    +			},
     			args: args{
     				ctx:           context.Background(),
     				oldPassword:   "password",
    @@ -796,8 +799,10 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			},
     		},
     		{
    -			name:   "old password missing, invalid argument error",
    -			fields: fields{},
    +			name: "old password missing, invalid argument error",
    +			fields: fields{
    +				tarpit: expectTarpit(0),
    +			},
     			args: args{
     				ctx:           context.Background(),
     				userID:        "user1",
    @@ -810,8 +815,10 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			},
     		},
     		{
    -			name:   "new password missing, invalid argument error",
    -			fields: fields{},
    +			name: "new password missing, invalid argument error",
    +			fields: fields{
    +				tarpit: expectTarpit(0),
    +			},
     			args: args{
     				ctx:           context.Background(),
     				userID:        "user1",
    @@ -824,8 +831,10 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			},
     		},
     		{
    -			name:   "user not existing, precondition error",
    -			fields: fields{},
    +			name: "user not existing, precondition error",
    +			fields: fields{
    +				tarpit: expectTarpit(0),
    +			},
     			args: args{
     				ctx:           context.Background(),
     				userID:        "user1",
    @@ -844,6 +853,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "existing password empty, precondition error",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -878,6 +888,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "password not matching complexity policy, invalid argument error",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -914,6 +925,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     							false,
     							"")),
     				),
    +				expectFilter(), // recheck of user locking relevant events
     				expectFilter(
     					eventFromEventPusher(
     						org.NewPasswordComplexityPolicyAddedEvent(
    @@ -936,6 +948,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "password not matching, invalid argument error",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(1),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -972,6 +985,89 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     							false,
     							"")),
     				),
    +				expectFilter(), // recheck of user locking relevant events
    +				expectFilter(
    +					eventFromEventPusher(
    +						org.NewLockoutPolicyAddedEvent(context.Background(),
    +							&org.NewAggregate("org1").Aggregate,
    +							0,
    +							0,
    +							false,
    +						),
    +					),
    +				),
    +				expectPush(
    +					user.NewHumanPasswordCheckFailedEvent(context.Background(),
    +						&user.NewAggregate("user1", "org1").Aggregate,
    +						nil,
    +					),
    +				),
    +			},
    +			res: res{
    +				err: zerrors.IsErrorInvalidArgument,
    +			},
    +		},
    +		{
    +			name: "password not matching, lockout policy active, invalid argument error",
    +			fields: fields{
    +				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(1),
    +			},
    +			args: args{
    +				ctx:           context.Background(),
    +				userID:        "user1",
    +				oldPassword:   "password-old",
    +				newPassword:   "password1",
    +				resourceOwner: "org1",
    +			},
    +			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(), // recheck of user locking relevant events
    +				expectFilter(
    +					eventFromEventPusher(
    +						org.NewLockoutPolicyAddedEvent(context.Background(),
    +							&org.NewAggregate("org1").Aggregate,
    +							1,
    +							0,
    +							false,
    +						),
    +					),
    +				),
    +				expectPush(
    +					user.NewHumanPasswordCheckFailedEvent(context.Background(),
    +						&user.NewAggregate("user1", "org1").Aggregate,
    +						nil,
    +					),
    +					user.NewUserLockedEvent(context.Background(),
    +						&user.NewAggregate("user1", "org1").Aggregate,
    +					),
    +				),
     			},
     			res: res{
     				err: zerrors.IsErrorInvalidArgument,
    @@ -981,6 +1077,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "change password, ok",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1017,6 +1114,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     							false,
     							"")),
     				),
    +				expectFilter(), // recheck of user locking relevant events
     				expectFilter(
     					eventFromEventPusher(
     						org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
    @@ -1048,6 +1146,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "change password with userAgentID, ok",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1085,6 +1184,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     							false,
     							"")),
     				),
    +				expectFilter(), // recheck of user locking relevant events
     				expectFilter(
     					eventFromEventPusher(
     						org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
    @@ -1116,6 +1216,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			name: "change password with changeRequired, ok",
     			fields: fields{
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:            context.Background(),
    @@ -1154,6 +1255,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     							false,
     							"")),
     				),
    +				expectFilter(), // recheck of user locking relevant events
     				expectFilter(
     					eventFromEventPusher(
     						org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
    @@ -1185,8 +1287,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     	for _, tt := range tests {
     		t.Run(tt.name, func(t *testing.T) {
     			r := &Commands{
    -				eventstore:         eventstoreExpect(t, tt.expect...),
    +				eventstore:         expectEventstore(tt.expect...)(t),
     				userPasswordHasher: tt.fields.userPasswordHasher,
    +				tarpit:             tt.fields.tarpit.tarpit,
     			}
     			got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.userAgentID, tt.args.changeRequired)
     			if tt.res.err == nil {
    @@ -1198,6 +1301,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
     			if tt.res.err == nil {
     				assertObjectDetails(t, tt.res.want, got)
     			}
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    @@ -1597,6 +1701,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     	type fields struct {
     		eventstore         func(*testing.T) *eventstore.Eventstore
     		userPasswordHasher *crypto.Hasher
    +		tarpit             Tarpit
     	}
     	type args struct {
     		ctx           context.Context
    @@ -1618,6 +1723,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     			name: "userid missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1632,6 +1738,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     			name: "password missing, invalid argument error",
     			fields: fields{
     				eventstore: expectEventstore(),
    +				tarpit:     expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1649,6 +1756,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					expectFilter(),
     					expectFilter(),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1689,6 +1797,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     						),
     					),
     				),
    +				tarpit: expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1730,6 +1839,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     					expectFilter(),
     				),
    +				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1791,6 +1902,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     						),
     					),
     				),
    +				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1848,6 +1961,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -1933,6 +2047,97 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(1),
    +			},
    +			args: args{
    +				ctx:           context.Background(),
    +				userID:        "user1",
    +				password:      "password1",
    +				resourceOwner: "org1",
    +				authReq: &domain.AuthRequest{
    +					ID:      "request1",
    +					AgentID: "agent1",
    +				},
    +			},
    +			res: res{
    +				err: zerrors.IsErrorInvalidArgument,
    +			},
    +		},
    +		{
    +			name: "password not matching, ignore unknow usernames (no tarpit), precondition error",
    +			fields: fields{
    +				eventstore: expectEventstore(
    +					expectFilter(
    +						eventFromEventPusher(
    +							org.NewLoginPolicyAddedEvent(context.Background(),
    +								&org.NewAggregate("org1").Aggregate,
    +								true,
    +								false,
    +								false,
    +								false,
    +								false,
    +								false,
    +								true,
    +								false,
    +								false,
    +								false,
    +								domain.PasswordlessTypeNotAllowed,
    +								"",
    +								time.Hour*1,
    +								time.Hour*2,
    +								time.Hour*3,
    +								time.Hour*4,
    +								time.Hour*5,
    +							),
    +						),
    +					),
    +					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(),
    +					expectFilter(
    +						eventFromEventPusher(
    +							org.NewLockoutPolicyAddedEvent(context.Background(),
    +								&org.NewAggregate("org1").Aggregate,
    +								0, 0, false,
    +							)),
    +					),
    +					expectPush(
    +						user.NewHumanPasswordCheckFailedEvent(context.Background(),
    +							&user.NewAggregate("user1", "org1").Aggregate,
    +							&user.AuthRequestInfo{
    +								ID:          "request1",
    +								UserAgentID: "agent1",
    +							},
    +						),
    +					),
    +				),
    +				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2026,6 +2231,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(1),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2108,6 +2314,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2193,6 +2400,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2270,6 +2478,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2366,6 +2575,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     					),
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:           context.Background(),
    @@ -2385,6 +2595,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     			r := &Commands{
     				eventstore:         tt.fields.eventstore(t),
     				userPasswordHasher: tt.fields.userPasswordHasher,
    +				tarpit:             tt.fields.tarpit.tarpit,
     			}
     			err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq)
     			if tt.res.err == nil {
    @@ -2393,6 +2604,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
     			if tt.res.err != nil && !tt.res.err(err) {
     				t.Errorf("got wrong err: %v ", err)
     			}
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    
  • internal/command/user_v2_human.go+1 1 modified
    @@ -493,7 +493,7 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com
     	}
     	// ...or old password
     	if password.OldPassword != "" {
    -		verification = c.checkCurrentPassword(password.Password, password.EncodedPasswordHash, password.OldPassword, wm.PasswordEncodedHash)
    +		verification = c.checkCurrentPassword(password.Password, password.EncodedPasswordHash, password.OldPassword, wm, c.tarpit)
     	}
     	cmd, err := c.setPasswordCommand(
     		ctx,
    
  • internal/command/user_v2_human_test.go+58 3 modified
    @@ -2207,6 +2207,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     		checkPermission             domain.PermissionCheck
     		defaultSecretGenerators     *SecretGenerators
     		defaultEmailCodeURLTemplate func(ctx context.Context) string
    +		tarpit                      Tarpit
     	}
     	type args struct {
     		ctx     context.Context
    @@ -2243,6 +2244,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					expectFilter(),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2268,6 +2270,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckNotAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2289,6 +2292,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					expectFilter(),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2334,6 +2338,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2381,6 +2386,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2408,6 +2414,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2435,6 +2442,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckNotAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2483,6 +2491,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2517,6 +2526,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2572,6 +2582,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				checkPermission:             newMockPermissionCheckAllowed(),
     				newCode:                     mockEncryptedCode("emailCode", time.Hour),
     				defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
    +				tarpit:                      expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2602,6 +2613,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2631,6 +2643,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckNotAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2668,6 +2681,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2703,6 +2717,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2753,6 +2768,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				checkPermission:             newMockPermissionCheckAllowed(),
     				newCode:                     mockEncryptedCode("emailCode", time.Hour),
     				defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
    +				tarpit:                      expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2835,6 +2851,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				checkPermission:             newMockPermissionCheckAllowed(),
     				newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
     				defaultSecretGenerators:     defaultGenerators,
    +				tarpit:                      expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2910,6 +2927,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				checkPermission:             newMockPermissionCheckAllowed(),
     				newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
     				defaultSecretGenerators:     defaultGenerators,
    +				tarpit:                      expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2940,6 +2958,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckNotAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -2977,6 +2996,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3012,6 +3032,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     					),
     				),
     				checkPermission: newMockPermissionCheckAllowed(),
    +				tarpit:          expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3091,6 +3112,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				checkPermission:             newMockPermissionCheckAllowed(),
     				newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
     				defaultSecretGenerators:     defaultGenerators,
    +				tarpit:                      expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3118,6 +3140,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				eventstore:         expectEventstore(),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3151,6 +3174,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3183,6 +3207,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     							),
     						),
     					),
    +					expectFilter(), // recheck of user locking relevant events
     					expectFilter(
     						eventFromEventPusher(
     							org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
    @@ -3198,6 +3223,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3222,6 +3248,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				eventstore:         expectEventstore(),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3256,6 +3283,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
     				checkPermission:    newMockPermissionCheckNotAllowed(),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3310,6 +3338,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				userPasswordHasher: mockPasswordHasher("x"),
     				checkPermission:    newMockPermissionCheckAllowed(),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3343,6 +3372,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     							),
     						),
     					),
    +					expectFilter(), // recheck of user locking relevant events
     					expectFilter(
     						eventFromEventPusher(
     							org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
    @@ -3366,6 +3396,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3400,9 +3431,27 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     							),
     						),
     					),
    +					expectFilter(), // recheck of user locking relevant events
    +					expectFilter(
    +						eventFromEventPusher(
    +							org.NewLockoutPolicyAddedEvent(context.Background(),
    +								&userAgg.Aggregate,
    +								0,
    +								0,
    +								false,
    +							),
    +						),
    +					),
    +					expectPush(
    +						user.NewHumanPasswordCheckFailedEvent(context.Background(),
    +							&userAgg.Aggregate,
    +							nil,
    +						),
    +					),
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(1),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3474,6 +3523,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3528,6 +3578,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3592,6 +3643,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3650,6 +3702,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
    @@ -3672,7 +3725,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     			},
     		},
     		{
    -			name: "change human password and password encoded, password code, encoded used",
    +			name: "change human password encoded, old password, ok",
     			fields: fields{
     				eventstore: expectEventstore(
     					expectFilter(
    @@ -3724,15 +3777,15 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				),
     				checkPermission:    newMockPermissionCheckAllowed(),
     				userPasswordHasher: mockPasswordHasher("x"),
    +				tarpit:             expectTarpit(0),
     			},
     			args: args{
     				ctx:   context.Background(),
     				orgID: "org1",
     				human: &ChangeHuman{
     					Password: &Password{
    -						Password:            "passwordnotused",
    +						OldPassword:         "password",
     						EncodedPasswordHash: "$plain$x$password2",
    -						PasswordCode:        "code",
     						ChangeRequired:      true,
     					},
     				},
    @@ -3758,6 +3811,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				defaultSecretGenerators:     tt.fields.defaultSecretGenerators,
     				userEncryption:              tt.args.codeAlg,
     				defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
    +				tarpit:                      tt.fields.tarpit.tarpit,
     			}
     			err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg)
     			if tt.res.err == nil {
    @@ -3773,6 +3827,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
     				assert.Equal(t, tt.res.wantEmailCode, tt.args.human.EmailCode)
     				assert.Equal(t, tt.res.wantPhoneCode, tt.args.human.PhoneCode)
     			}
    +			tt.fields.tarpit.metExpectedCalls(t)
     		})
     	}
     }
    
  • internal/command/user_v2_model.go+21 0 modified
    @@ -80,6 +80,26 @@ type UserV2WriteModel struct {
     	Metadata           map[string][]byte
     }
     
    +func (wm *UserV2WriteModel) GetUserState() domain.UserState {
    +	return wm.UserState
    +}
    +
    +func (wm *UserV2WriteModel) GetPasswordCheckFailedCount() uint64 {
    +	return wm.PasswordCheckFailedCount
    +}
    +
    +func (wm *UserV2WriteModel) GetEncodedHash() string {
    +	return wm.PasswordEncodedHash
    +}
    +
    +func (wm *UserV2WriteModel) GetResourceOwner() string {
    +	return wm.ResourceOwner
    +}
    +
    +func (wm *UserV2WriteModel) GetWriteModel() *eventstore.WriteModel {
    +	return &wm.WriteModel
    +}
    +
     func NewUserExistsWriteModel(userID, resourceOwner string) *UserV2WriteModel {
     	return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine())
     }
    @@ -292,6 +312,7 @@ func (wm *UserV2WriteModel) Reduce() error {
     		case *user.HumanPasswordChangedEvent:
     			wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash)
     			wm.PasswordChangeRequired = e.ChangeRequired
    +			wm.PasswordCheckFailedCount = 0
     			wm.EmptyPasswordCode()
     		case *user.HumanPasswordCodeAddedEvent:
     			wm.SetPasswordCode(e)
    
  • internal/config/systemdefaults/system_defaults.go+2 1 modified
    @@ -2,7 +2,7 @@ package systemdefaults
     
     import (
     	"time"
    -
    +	
     	"github.com/zitadel/zitadel/internal/crypto"
     )
     
    @@ -11,6 +11,7 @@ type SystemDefaults struct {
     	PasswordHasher       crypto.HashConfig
     	SecretHasher         crypto.HashConfig
     	Multifactors         MultifactorConfig
    +	Tarpit               TarpitConfig
     	DomainVerification   DomainVerification
     	Notifications        Notifications
     	KeyConfig            KeyConfig
    
  • internal/config/systemdefaults/tarpit.go+34 0 added
    @@ -0,0 +1,34 @@
    +package systemdefaults
    +
    +import "time"
    +
    +type TarpitConfig struct {
    +	// After how many failed attempts, the tarpit should start.
    +	MinFailedAttempts uint64
    +	// The seconds that will be added per step.
    +	StepDuration time.Duration
    +	// The failed attempts that are needed to increase the tarpit by one step.
    +	StepSize uint64
    +	// The maximum duration the tarpit can reach.
    +	MaxDuration time.Duration
    +}
    +
    +func (t *TarpitConfig) Tarpit() func(failedCount uint64) {
    +	return func(failedCount uint64) {
    +		time.Sleep(t.duration(failedCount))
    +	}
    +}
    +
    +func (t *TarpitConfig) duration(failedCount uint64) time.Duration {
    +	if failedCount < t.MinFailedAttempts {
    +		return 0
    +	}
    +	// calculate the step we are at
    +	// every StepSize failed attempts increase the step by one
    +	step := (failedCount - t.MinFailedAttempts) / t.StepSize
    +	duration := time.Duration(step) * t.StepDuration
    +	if duration < t.MaxDuration {
    +		return duration
    +	}
    +	return t.MaxDuration
    +}
    
  • internal/config/systemdefaults/tarpit_test.go+83 0 added
    @@ -0,0 +1,83 @@
    +package systemdefaults
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestTarpitConfig_duration(t *testing.T) {
    +	type fields struct {
    +		MinFailedAttempts uint64
    +		StepDuration      time.Duration
    +		StepSize          uint64
    +		MaxDuration       time.Duration
    +	}
    +	type args struct {
    +		failedCount uint64
    +	}
    +	tests := []struct {
    +		name   string
    +		fields fields
    +		args   args
    +		want   time.Duration
    +	}{
    +		{
    +			"no tarpit",
    +			fields{
    +				MinFailedAttempts: 2,
    +				StepDuration:      time.Second,
    +				StepSize:          1,
    +				MaxDuration:       5 * time.Second,
    +			},
    +			args{failedCount: 1},
    +			0,
    +		},
    +		{
    +			"first step",
    +			fields{
    +				MinFailedAttempts: 2,
    +				StepDuration:      time.Second,
    +				StepSize:          1,
    +				MaxDuration:       5 * time.Second,
    +			},
    +			args{failedCount: 3},
    +			time.Second,
    +		},
    +		{
    +			"second step",
    +			fields{
    +				MinFailedAttempts: 2,
    +				StepDuration:      time.Second,
    +				StepSize:          1,
    +				MaxDuration:       5 * time.Second,
    +			},
    +			args{failedCount: 4},
    +			2 * time.Second,
    +		},
    +		{
    +			"exceeding max duration",
    +			fields{
    +				MinFailedAttempts: 2,
    +				StepDuration:      time.Second,
    +				StepSize:          1,
    +				MaxDuration:       5 * time.Second,
    +			},
    +			args{failedCount: 20},
    +			5 * time.Second,
    +		},
    +	}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			c := &TarpitConfig{
    +				MinFailedAttempts: tt.fields.MinFailedAttempts,
    +				StepDuration:      tt.fields.StepDuration,
    +				StepSize:          tt.fields.StepSize,
    +				MaxDuration:       tt.fields.MaxDuration,
    +			}
    +			got := c.duration(tt.args.failedCount)
    +			assert.Equal(t, tt.want, got)
    +		})
    +	}
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.