fix: respect lockout policy on password change (with old password) and add tar pit for checks

# Which Problems Are Solved

While the lockout policy was correctly applied on the session API and other authentication and management endpoints , it had no effect on the user service v2 endpoints.

# How the Problems Are Solved

- Correctly apply lockout policy on the user service v2 endpoints.
- Added tar pitting to auth factor checks (authentication and management API) to prevent brute-force attacks or denial of service because of user lockouts.
- Tar pitting is not active if `IgnoreUnknownUsername` option is active to prevent leaking information whether a user exists or not.

# Additional Changes

None

# Additional Context

- requires backports

* cleanup

(cherry picked from commit b8db8cdf9c)
This commit is contained in:
Livio Spring
2025-10-29 10:07:35 +01:00
parent 7520450e11
commit d3713dfaed
17 changed files with 618 additions and 49 deletions

View File

@@ -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
}