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

@@ -740,6 +740,15 @@ SystemDefaults:
# If this is empty, the issuer is the requested domain # If this is empty, the issuer is the requested domain
# This is helpful in scenarios with multiple ZITADEL environments or virtual instances # This is helpful in scenarios with multiple ZITADEL environments or virtual instances
Issuer: "ZITADEL" # ZITADEL_SYSTEMDEFAULTS_MULTIFACTORS_OTP_ISSUER 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: DomainVerification:
VerificationGenerator: VerificationGenerator:
Length: 32 # ZITADEL_SYSTEMDEFAULTS_DOMAINVERIFICATION_VERIFICATIONGENERATOR_LENGTH Length: 32 # ZITADEL_SYSTEMDEFAULTS_DOMAINVERIFICATION_VERIFICATIONGENERATOR_LENGTH

View File

@@ -70,6 +70,7 @@ type Commands struct {
defaultRefreshTokenLifetime time.Duration defaultRefreshTokenLifetime time.Duration
defaultRefreshTokenIdleLifetime time.Duration defaultRefreshTokenIdleLifetime time.Duration
phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
tarpit func(failedAttempts uint64)
multifactors domain.MultifactorConfigs multifactors domain.MultifactorConfigs
webauthnConfig *webauthn_helper.Config webauthnConfig *webauthn_helper.Config
@@ -214,6 +215,7 @@ func StartCommands(
repo.newHashedSecret = newHashedSecretWithDefault(secretHasher, defaultSecretGenerators.ClientSecret) repo.newHashedSecret = newHashedSecretWithDefault(secretHasher, defaultSecretGenerators.ClientSecret)
} }
repo.phoneCodeVerifier = repo.phoneCodeVerifierFromConfig repo.phoneCodeVerifier = repo.phoneCodeVerifierFromConfig
repo.tarpit = defaults.Tarpit.Tarpit()
return repo, nil return repo, nil
} }

View File

@@ -43,6 +43,7 @@ type SessionCommands struct {
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
now func() time.Time now func() time.Time
maxIdPIntentLifetime time.Duration maxIdPIntentLifetime time.Duration
tarpit func(failedAttempts uint64)
} }
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
@@ -60,6 +61,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
getCodeVerifier: c.phoneCodeVerifierFromConfig, getCodeVerifier: c.phoneCodeVerifierFromConfig,
now: time.Now, now: time.Now,
maxIdPIntentLifetime: c.maxIdPIntentLifetime, 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 // CheckPassword defines a password check to be executed for a session update
func CheckPassword(password string) SessionCommand { func CheckPassword(password string) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) { 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 { if err != nil {
return commands, err return commands, err
} }
@@ -135,6 +137,7 @@ func CheckTOTP(code string) SessionCommand {
cmd.eventstore.FilterToQueryReducer, cmd.eventstore.FilterToQueryReducer,
cmd.totpAlg, cmd.totpAlg,
nil, nil,
cmd.tarpit,
) )
if err != nil { if err != nil {
return commands, err return commands, err

View File

@@ -143,6 +143,7 @@ func CheckOTPSMS(code string) SessionCommand {
cmd.getCodeVerifier, cmd.getCodeVerifier,
succeededEvent, succeededEvent,
failedEvent, failedEvent,
cmd.tarpit,
) )
if err != nil { if err != nil {
return commands, err return commands, err
@@ -183,6 +184,7 @@ func CheckOTPEmail(code string) SessionCommand {
nil, // email currently always uses local code checks nil, // email currently always uses local code checks
succeededEvent, succeededEvent,
failedEvent, failedEvent,
cmd.tarpit,
) )
if err != nil { if err != nil {
return commands, err return commands, err

View File

@@ -1037,6 +1037,9 @@ func TestCheckOTPSMS(t *testing.T) {
now: func() time.Time { now: func() time.Time {
return testNow return testNow
}, },
tarpit: func(failedAttempts uint64) {
},
} }
gotCmds, err := cmd(context.Background(), cmds) gotCmds, err := cmd(context.Background(), cmds)
@@ -1053,6 +1056,7 @@ func TestCheckOTPEmail(t *testing.T) {
userID string userID string
otpCodeChallenge *OTPCode otpCodeChallenge *OTPCode
otpAlg crypto.EncryptionAlgorithm otpAlg crypto.EncryptionAlgorithm
tarpit Tarpit
} }
type args struct { type args struct {
code string code string
@@ -1073,6 +1077,7 @@ func TestCheckOTPEmail(t *testing.T) {
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
userID: "", userID: "",
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1086,6 +1091,7 @@ func TestCheckOTPEmail(t *testing.T) {
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
userID: "userID", userID: "userID",
tarpit: expectTarpit(0),
}, },
args: args{}, args: args{},
res: res{ res: res{
@@ -1099,6 +1105,7 @@ func TestCheckOTPEmail(t *testing.T) {
expectFilter(), expectFilter(),
), ),
userID: "userID", userID: "userID",
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1117,6 +1124,7 @@ func TestCheckOTPEmail(t *testing.T) {
), ),
userID: "userID", userID: "userID",
otpCodeChallenge: nil, otpCodeChallenge: nil,
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1153,6 +1161,7 @@ func TestCheckOTPEmail(t *testing.T) {
CreationDate: testNow.Add(-10 * time.Minute), CreationDate: testNow.Add(-10 * time.Minute),
}, },
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1192,6 +1201,7 @@ func TestCheckOTPEmail(t *testing.T) {
CreationDate: testNow.Add(-10 * time.Minute), CreationDate: testNow.Add(-10 * time.Minute),
}, },
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1225,6 +1235,7 @@ func TestCheckOTPEmail(t *testing.T) {
CreationDate: testNow, CreationDate: testNow,
}, },
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1261,6 +1272,7 @@ func TestCheckOTPEmail(t *testing.T) {
CreationDate: testNow, CreationDate: testNow,
}, },
otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
code: "code", code: "code",
@@ -1289,12 +1301,39 @@ func TestCheckOTPEmail(t *testing.T) {
now: func() time.Time { now: func() time.Time {
return testNow return testNow
}, },
tarpit: tt.fields.tarpit.tarpit,
} }
gotCmds, err := cmd(context.Background(), cmds) gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err) assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.errorCommands, gotCmds) assert.Equal(t, tt.res.errorCommands, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands) 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
}

View File

@@ -1091,6 +1091,7 @@ func TestCheckTOTP(t *testing.T) {
type fields struct { type fields struct {
sessionWriteModel *SessionWriteModel sessionWriteModel *SessionWriteModel
eventstore func(*testing.T) *eventstore.Eventstore eventstore func(*testing.T) *eventstore.Eventstore
tarpit Tarpit
} }
tests := []struct { tests := []struct {
@@ -1109,6 +1110,7 @@ func TestCheckTOTP(t *testing.T) {
aggregate: sessAgg, aggregate: sessAgg,
}, },
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"), wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
}, },
@@ -1124,6 +1126,7 @@ func TestCheckTOTP(t *testing.T) {
eventstore: expectEventstore( eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe), expectFilterError(io.ErrClosedPipe),
), ),
tarpit: expectTarpit(0),
}, },
wantErr: io.ErrClosedPipe, 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"), 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)), eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 0, 0, false)),
), ),
), ),
tarpit: expectTarpit(1),
}, },
wantErrorCommands: []eventstore.Command{ wantErrorCommands: []eventstore.Command{
user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil), user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
@@ -1198,6 +1203,7 @@ func TestCheckTOTP(t *testing.T) {
eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 1, 1, false)), eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 1, 1, false)),
), ),
), ),
tarpit: expectTarpit(1),
}, },
wantErrorCommands: []eventstore.Command{ wantErrorCommands: []eventstore.Command{
user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil), user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
@@ -1225,6 +1231,7 @@ func TestCheckTOTP(t *testing.T) {
), ),
expectFilter(), // recheck expectFilter(), // recheck
), ),
tarpit: expectTarpit(0),
}, },
wantEventCommands: []eventstore.Command{ wantEventCommands: []eventstore.Command{
user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, nil), user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, nil),
@@ -1253,6 +1260,7 @@ func TestCheckTOTP(t *testing.T) {
user.NewUserLockedEvent(ctx, userAgg), user.NewUserLockedEvent(ctx, userAgg),
), ),
), ),
tarpit: expectTarpit(0),
}, },
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked"), wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked"),
}, },
@@ -1264,11 +1272,13 @@ func TestCheckTOTP(t *testing.T) {
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
totpAlg: cryptoAlg, totpAlg: cryptoAlg,
now: func() time.Time { return testNow }, now: func() time.Time { return testNow },
tarpit: tt.fields.tarpit.tarpit,
} }
gotCmds, err := CheckTOTP(tt.code)(ctx, cmd) gotCmds, err := CheckTOTP(tt.code)(ctx, cmd)
require.ErrorIs(t, err, tt.wantErr) require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantErrorCommands, gotCmds) assert.Equal(t, tt.wantErrorCommands, gotCmds)
assert.Equal(t, tt.wantEventCommands, cmd.eventCommands) assert.Equal(t, tt.wantEventCommands, cmd.eventCommands)
tt.fields.tarpit.metExpectedCalls(t)
}) })
} }
} }

View File

@@ -170,6 +170,7 @@ func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resource
c.eventstore.FilterToQueryReducer, c.eventstore.FilterToQueryReducer,
c.multifactors.OTP.CryptoMFA, c.multifactors.OTP.CryptoMFA,
authRequestDomainToAuthRequestInfo(authRequest), authRequestDomainToAuthRequestInfo(authRequest),
c.tarpit,
) )
_, pushErr := c.eventstore.Push(ctx, commands...) _, pushErr := c.eventstore.Push(ctx, commands...)
@@ -183,6 +184,7 @@ func checkTOTP(
queryReducer func(ctx context.Context, r eventstore.QueryReducer) error, queryReducer func(ctx context.Context, r eventstore.QueryReducer) error,
alg crypto.EncryptionAlgorithm, alg crypto.EncryptionAlgorithm,
optionalAuthRequestInfo *user.AuthRequestInfo, optionalAuthRequestInfo *user.AuthRequestInfo,
tarpit func(failedAttempts uint64),
) ([]eventstore.Command, error) { ) ([]eventstore.Command, error) {
if userID == "" { if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing") 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 { if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg)) commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
} }
tarpit(existingOTP.CheckFailedCount + 1)
return commands, verifyErr return commands, verifyErr
} }
@@ -374,6 +377,7 @@ func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceO
c.phoneCodeVerifier, c.phoneCodeVerifier,
succeededEvent, succeededEvent,
failedEvent, failedEvent,
c.tarpit,
) )
if len(commands) > 0 { if len(commands) > 0 {
_, pushErr := c.eventstore.Push(ctx, commands...) _, 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 nil, // email currently always uses local code checks
succeededEvent, succeededEvent,
failedEvent, failedEvent,
c.tarpit,
) )
if len(commands) > 0 { if len(commands) > 0 {
_, pushErr := c.eventstore.Push(ctx, commands...) _, pushErr := c.eventstore.Push(ctx, commands...)
@@ -576,6 +581,7 @@ func checkOTP(
alg crypto.EncryptionAlgorithm, alg crypto.EncryptionAlgorithm,
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error), getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error),
checkSucceededEvent, checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command, checkSucceededEvent, checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
tarpit func(failedAttempts uint64),
) ([]eventstore.Command, error) { ) ([]eventstore.Command, error) {
if userID == "" { if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing") 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 { if lockoutPolicy != nil && lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount()+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg)) commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
} }
tarpit(existingOTP.CheckFailedCount() + 1)
return commands, verifyErr return commands, verifyErr
} }

View File

@@ -1940,6 +1940,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
eventstore func(*testing.T) *eventstore.Eventstore eventstore func(*testing.T) *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm userEncryption crypto.EncryptionAlgorithm
phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) phoneCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
tarpit Tarpit
} }
type ( type (
args struct { args struct {
@@ -1964,6 +1965,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
name: "userid missing, invalid argument error", name: "userid missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -1979,6 +1981,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
name: "code missing, invalid argument error", name: "code missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -1996,6 +1999,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
eventstore: expectEventstore( eventstore: expectEventstore(
expectFilter(), expectFilter(),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2019,6 +2023,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2088,6 +2093,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2169,6 +2175,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2239,6 +2246,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2301,6 +2309,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2380,6 +2389,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
sender.EXPECT().VerifyCode("verificationID", "code").Return(nil) sender.EXPECT().VerifyCode("verificationID", "code").Return(nil)
return sender, nil return sender, nil
}, },
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -2409,9 +2419,11 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
userEncryption: tt.fields.userEncryption, userEncryption: tt.fields.userEncryption,
phoneCodeVerifier: tt.fields.phoneCodeVerifier, 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) 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) 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 { type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore eventstore func(*testing.T) *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm userEncryption crypto.EncryptionAlgorithm
tarpit Tarpit
} }
type ( type (
args struct { args struct {
@@ -3139,6 +3152,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
name: "userid missing, invalid argument error", name: "userid missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3154,6 +3168,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
name: "code missing, invalid argument error", name: "code missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3171,6 +3186,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
eventstore: expectEventstore( eventstore: expectEventstore(
expectFilter(), expectFilter(),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3194,6 +3210,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3262,6 +3279,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3342,6 +3360,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3411,6 +3430,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3472,6 +3492,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: ctx, ctx: ctx,
@@ -3498,9 +3519,11 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
userEncryption: tt.fields.userEncryption, 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) 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) assert.ErrorIs(t, err, tt.res.err)
tt.fields.tarpit.metExpectedCalls(t)
}) })
} }
} }

View File

@@ -103,7 +103,7 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
"", "",
userAgentID, userAgentID,
changeRequired, 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 // checkCurrentPassword returns a password check as [setPasswordVerification] implementation
func (c *Commands) checkCurrentPassword( func (c *Commands) checkCurrentPassword(
newPassword, newEncodedPassword, currentPassword, currentEncodePassword string, newPassword, newEncodedPassword, currentPassword string,
wm HumanPasswordCheckWriteModel,
tarpit func(failedAttempts uint64),
) setPasswordVerification { ) setPasswordVerification {
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 // in case the new password is already encoded, we only need to verify the current
if newEncodedPassword != "" { if newEncodedPassword != "" {
return func(ctx context.Context) (_ string, err error) {
_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify") _, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
_, err = c.userPasswordHasher.Verify(currentEncodePassword, currentPassword) _, err = c.userPasswordHasher.Verify(hash, password)
spanPasswap.EndWithError(err) spanPasswap.EndWithError(err)
return "", convertPasswapErr(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)
} }
commands, updated, err := verifyPasswordWithLockoutPolicy(ctx, wm, currentPassword, c.eventstore, verify, nil, tarpit)
// otherwise let's directly verify and return the new generate hash, so we can reuse it in the event // The verification was successful, and we might have an updated hash.
return func(ctx context.Context) (string, error) { if err == nil {
return c.verifyAndUpdatePassword(ctx, currentEncodePassword, currentPassword, newPassword) 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 { if !loginPolicy.AllowUsernamePassword {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-Dft32", "Errors.Org.LoginPolicy.UsernamePasswordNotAllowed") 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 { if len(commands) == 0 {
return err return err
} }
@@ -353,7 +383,7 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
return err 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 == "" { if userID == "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing") return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing")
} }
@@ -362,36 +392,49 @@ func checkPassword(ctx context.Context, userID, password string, es *eventstore.
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !wm.UserState.Exists() { commands, _, err := verifyPasswordWithLockoutPolicy(ctx, wm, password, es, hasher.Verify, optionalAuthRequestInfo, tarpit)
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound") 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.UserState == domain.UserStateLocked { if wm.GetUserState() == domain.UserStateLocked {
wrongPasswordError := &commandErrors.WrongPasswordError{ 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 == "" { if wm.GetEncodedHash() == "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet") 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") ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
updated, err := hasher.Verify(wm.EncodedHash, password) updated, err := verify(wm.GetEncodedHash(), password)
spanPasswordComparison.EndWithError(err) spanPasswordComparison.EndWithError(err)
err = convertLoginPasswapErr(wm.PasswordCheckFailedCount+1, err) err = convertLoginPasswapErr(wm.GetPasswordCheckFailedCount()+1, err)
commands := make([]eventstore.Command, 0, 2) commands := make([]eventstore.Command, 0, 2)
// recheck for additional events (failed password checks or locks) // recheck for additional events (failed password checks or locks)
recheckErr := es.FilterToQueryReducer(ctx, wm) recheckErr := es.FilterToQueryReducer(ctx, wm)
if recheckErr != nil { if recheckErr != nil {
return nil, recheckErr return nil, "", recheckErr
} }
if wm.UserState == domain.UserStateLocked { if wm.GetUserState() == domain.UserStateLocked {
wrongPasswordError := &commandErrors.WrongPasswordError{ 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 { if err == nil {
@@ -399,17 +442,23 @@ func checkPassword(ctx context.Context, userID, password string, es *eventstore.
if updated != "" { if updated != "" {
commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated)) commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated))
} }
return commands, nil return commands, updated, nil
} }
commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, optionalAuthRequestInfo)) 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") 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)) 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) { func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {

View File

@@ -25,6 +25,26 @@ type HumanPasswordWriteModel struct {
UserState domain.UserState 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 { func NewHumanPasswordWriteModel(userID, resourceOwner string) *HumanPasswordWriteModel {
return &HumanPasswordWriteModel{ return &HumanPasswordWriteModel{
WriteModel: eventstore.WriteModel{ WriteModel: eventstore.WriteModel{

View File

@@ -760,6 +760,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
func TestCommandSide_ChangePassword(t *testing.T) { func TestCommandSide_ChangePassword(t *testing.T) {
type fields struct { type fields struct {
userPasswordHasher *crypto.Hasher userPasswordHasher *crypto.Hasher
tarpit Tarpit
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -783,7 +784,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
}{ }{
{ {
name: "userid missing, invalid argument error", name: "userid missing, invalid argument error",
fields: fields{}, fields: fields{
tarpit: expectTarpit(0),
},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
oldPassword: "password", oldPassword: "password",
@@ -797,7 +800,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
}, },
{ {
name: "old password missing, invalid argument error", name: "old password missing, invalid argument error",
fields: fields{}, fields: fields{
tarpit: expectTarpit(0),
},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
userID: "user1", userID: "user1",
@@ -811,7 +816,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
}, },
{ {
name: "new password missing, invalid argument error", name: "new password missing, invalid argument error",
fields: fields{}, fields: fields{
tarpit: expectTarpit(0),
},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
userID: "user1", userID: "user1",
@@ -825,7 +832,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
}, },
{ {
name: "user not existing, precondition error", name: "user not existing, precondition error",
fields: fields{}, fields: fields{
tarpit: expectTarpit(0),
},
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
userID: "user1", userID: "user1",
@@ -844,6 +853,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "existing password empty, precondition error", name: "existing password empty, precondition error",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -878,6 +888,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "password not matching complexity policy, invalid argument error", name: "password not matching complexity policy, invalid argument error",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -914,6 +925,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false, false,
"")), "")),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent( org.NewPasswordComplexityPolicyAddedEvent(
@@ -936,6 +948,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "password not matching, invalid argument error", name: "password not matching, invalid argument error",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -972,6 +985,89 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false, 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{ res: res{
err: zerrors.IsErrorInvalidArgument, err: zerrors.IsErrorInvalidArgument,
@@ -981,6 +1077,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "change password, ok", name: "change password, ok",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1017,6 +1114,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false, false,
"")), "")),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(), org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
@@ -1048,6 +1146,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "change password with userAgentID, ok", name: "change password with userAgentID, ok",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1085,6 +1184,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false, false,
"")), "")),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(), org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
@@ -1116,6 +1216,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name: "change password with changeRequired, ok", name: "change password with changeRequired, ok",
fields: fields{ fields: fields{
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1154,6 +1255,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false, false,
"")), "")),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(), org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
@@ -1185,8 +1287,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: eventstoreExpect(t, tt.expect...), eventstore: expectEventstore(tt.expect...)(t),
userPasswordHasher: tt.fields.userPasswordHasher, 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) 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 { if tt.res.err == nil {
@@ -1198,6 +1301,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
if tt.res.err == nil { if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, got) assertObjectDetails(t, tt.res.want, got)
} }
tt.fields.tarpit.metExpectedCalls(t)
}) })
} }
} }
@@ -1597,6 +1701,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
type fields struct { type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore eventstore func(*testing.T) *eventstore.Eventstore
userPasswordHasher *crypto.Hasher userPasswordHasher *crypto.Hasher
tarpit Tarpit
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -1618,6 +1723,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
name: "userid missing, invalid argument error", name: "userid missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1632,6 +1738,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
name: "password missing, invalid argument error", name: "password missing, invalid argument error",
fields: fields{ fields: fields{
eventstore: expectEventstore(), eventstore: expectEventstore(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1649,6 +1756,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
expectFilter(), expectFilter(),
expectFilter(), expectFilter(),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1689,6 +1797,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
), ),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1730,6 +1839,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
expectFilter(), expectFilter(),
), ),
userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1791,6 +1902,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1848,6 +1961,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -1933,6 +2047,97 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), 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{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2026,6 +2231,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2108,6 +2314,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2193,6 +2400,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2270,6 +2478,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2366,6 +2575,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
), ),
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2385,6 +2595,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher, 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) err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq)
if tt.res.err == nil { if tt.res.err == nil {
@@ -2393,6 +2604,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
if tt.res.err != nil && !tt.res.err(err) { if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err) t.Errorf("got wrong err: %v ", err)
} }
tt.fields.tarpit.metExpectedCalls(t)
}) })
} }
} }

View File

@@ -488,7 +488,7 @@ func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Com
} }
// ...or old password // ...or old password
if password.OldPassword != "" { 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( cmd, err := c.setPasswordCommand(
ctx, ctx,

View File

@@ -2100,6 +2100,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission domain.PermissionCheck checkPermission domain.PermissionCheck
defaultSecretGenerators *SecretGenerators defaultSecretGenerators *SecretGenerators
defaultEmailCodeURLTemplate func(ctx context.Context) string defaultEmailCodeURLTemplate func(ctx context.Context) string
tarpit Tarpit
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -2135,6 +2136,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
expectFilter(), expectFilter(),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2160,6 +2162,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckNotAllowed(), checkPermission: newMockPermissionCheckNotAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2181,6 +2184,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
expectFilter(), expectFilter(),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2224,6 +2228,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2251,6 +2256,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2278,6 +2284,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckNotAllowed(), checkPermission: newMockPermissionCheckNotAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2326,6 +2333,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2360,6 +2368,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2415,6 +2424,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour), newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2445,6 +2455,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2474,6 +2485,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckNotAllowed(), checkPermission: newMockPermissionCheckNotAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2511,6 +2523,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2546,6 +2559,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2596,6 +2610,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
newCode: mockEncryptedCode("emailCode", time.Hour), newCode: mockEncryptedCode("emailCode", time.Hour),
defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" }, defaultEmailCodeURLTemplate: func(ctx context.Context) string { return "http://example.com/{{.user}}/email/{{.code}}" },
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2678,6 +2693,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
defaultSecretGenerators: defaultGenerators, defaultSecretGenerators: defaultGenerators,
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2753,6 +2769,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
defaultSecretGenerators: defaultGenerators, defaultSecretGenerators: defaultGenerators,
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2783,6 +2800,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckNotAllowed(), checkPermission: newMockPermissionCheckNotAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2820,6 +2838,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2855,6 +2874,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2934,6 +2954,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour), newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
defaultSecretGenerators: defaultGenerators, defaultSecretGenerators: defaultGenerators,
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2961,6 +2982,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
eventstore: expectEventstore(), eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -2994,6 +3016,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3026,6 +3049,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(), org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
@@ -3041,6 +3065,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3065,6 +3090,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
eventstore: expectEventstore(), eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3099,6 +3125,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
checkPermission: newMockPermissionCheckNotAllowed(), checkPermission: newMockPermissionCheckNotAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3153,6 +3180,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3186,6 +3214,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck of user locking relevant events
expectFilter( expectFilter(
eventFromEventPusher( eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(), org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
@@ -3209,6 +3238,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3243,9 +3273,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(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(1),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3317,6 +3365,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3371,6 +3420,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3435,6 +3485,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3493,6 +3544,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
@@ -3515,7 +3567,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{ fields: fields{
eventstore: expectEventstore( eventstore: expectEventstore(
expectFilter( expectFilter(
@@ -3567,15 +3619,15 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
), ),
checkPermission: newMockPermissionCheckAllowed(), checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"), userPasswordHasher: mockPasswordHasher("x"),
tarpit: expectTarpit(0),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
human: &ChangeHuman{ human: &ChangeHuman{
Password: &Password{ Password: &Password{
Password: "passwordnotused", OldPassword: "password",
EncodedPasswordHash: "$plain$x$password2", EncodedPasswordHash: "$plain$x$password2",
PasswordCode: "code",
ChangeRequired: true, ChangeRequired: true,
}, },
}, },
@@ -3601,6 +3653,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
defaultSecretGenerators: tt.fields.defaultSecretGenerators, defaultSecretGenerators: tt.fields.defaultSecretGenerators,
userEncryption: tt.args.codeAlg, userEncryption: tt.args.codeAlg,
defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate, defaultEmailCodeURLTemplate: tt.fields.defaultEmailCodeURLTemplate,
tarpit: tt.fields.tarpit.tarpit,
} }
err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg) err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg)
if tt.res.err == nil { if tt.res.err == nil {
@@ -3616,6 +3669,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
assert.Equal(t, tt.res.wantEmailCode, tt.args.human.EmailCode) assert.Equal(t, tt.res.wantEmailCode, tt.args.human.EmailCode)
assert.Equal(t, tt.res.wantPhoneCode, tt.args.human.PhoneCode) assert.Equal(t, tt.res.wantPhoneCode, tt.args.human.PhoneCode)
} }
tt.fields.tarpit.metExpectedCalls(t)
}) })
} }
} }

View File

@@ -80,6 +80,26 @@ type UserV2WriteModel struct {
Metadata map[string][]byte 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 { func NewUserExistsWriteModel(userID, resourceOwner string) *UserV2WriteModel {
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine()) return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine())
} }
@@ -292,6 +312,7 @@ func (wm *UserV2WriteModel) Reduce() error {
case *user.HumanPasswordChangedEvent: case *user.HumanPasswordChangedEvent:
wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) wm.PasswordEncodedHash = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.PasswordChangeRequired = e.ChangeRequired wm.PasswordChangeRequired = e.ChangeRequired
wm.PasswordCheckFailedCount = 0
wm.EmptyPasswordCode() wm.EmptyPasswordCode()
case *user.HumanPasswordCodeAddedEvent: case *user.HumanPasswordCodeAddedEvent:
wm.SetPasswordCode(e) wm.SetPasswordCode(e)

View File

@@ -11,6 +11,7 @@ type SystemDefaults struct {
PasswordHasher crypto.HashConfig PasswordHasher crypto.HashConfig
SecretHasher crypto.HashConfig SecretHasher crypto.HashConfig
Multifactors MultifactorConfig Multifactors MultifactorConfig
Tarpit TarpitConfig
DomainVerification DomainVerification DomainVerification DomainVerification
Notifications Notifications Notifications Notifications
KeyConfig KeyConfig KeyConfig KeyConfig

View File

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

View File

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