mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-01 19:42:12 +00:00
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 commitb8db8cdf9c) (cherry picked from commitd3713dfaed)
This commit is contained in:
@@ -725,6 +725,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
|
||||
|
||||
@@ -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
|
||||
@@ -207,6 +208,7 @@ func StartCommands(
|
||||
repo.newHashedSecret = newHashedSecretWithDefault(secretHasher, defaultSecretGenerators.ClientSecret)
|
||||
}
|
||||
repo.phoneCodeVerifier = repo.phoneCodeVerifierFromConfig
|
||||
repo.tarpit = defaults.Tarpit.Tarpit()
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1128,6 +1128,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
sessionWriteModel *SessionWriteModel
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
tarpit Tarpit
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -1146,6 +1147,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
aggregate: sessAgg,
|
||||
},
|
||||
eventstore: expectEventstore(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
@@ -1161,6 +1163,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
@@ -1180,6 +1183,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady"),
|
||||
},
|
||||
@@ -1206,6 +1210,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),
|
||||
@@ -1235,6 +1240,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),
|
||||
@@ -1262,6 +1268,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
),
|
||||
expectFilter(), // recheck
|
||||
),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
wantEventCommands: []eventstore.Command{
|
||||
user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, nil),
|
||||
@@ -1290,6 +1297,7 @@ func TestCheckTOTP(t *testing.T) {
|
||||
user.NewUserLockedEvent(ctx, userAgg),
|
||||
),
|
||||
),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked"),
|
||||
},
|
||||
@@ -1301,11 +1309,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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,36 +392,49 @@ 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")
|
||||
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.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-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 {
|
||||
@@ -399,17 +442,23 @@ func checkPassword(ctx context.Context, userID, password string, es *eventstore.
|
||||
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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +475,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,
|
||||
|
||||
@@ -2096,6 +2096,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||
checkPermission domain.PermissionCheck
|
||||
defaultSecretGenerators *SecretGenerators
|
||||
tarpit Tarpit
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -2131,6 +2132,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2156,6 +2158,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2177,6 +2180,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2220,6 +2224,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2247,6 +2252,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2274,6 +2280,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2322,6 +2329,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2356,6 +2364,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2410,6 +2419,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockEncryptedCode("emailCode", time.Hour),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2440,6 +2450,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2469,6 +2480,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2506,6 +2518,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2541,6 +2554,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2590,6 +2604,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockEncryptedCode("emailCode", time.Hour),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2672,6 +2687,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
|
||||
defaultSecretGenerators: defaultGenerators,
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2747,6 +2763,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
|
||||
defaultSecretGenerators: defaultGenerators,
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2777,6 +2794,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2814,6 +2832,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2849,6 +2868,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2928,6 +2948,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("phoneCode", time.Hour),
|
||||
defaultSecretGenerators: defaultGenerators,
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2955,6 +2976,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
eventstore: expectEventstore(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -2988,6 +3010,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3020,6 +3043,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(), // recheck of user locking relevant events
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
@@ -3035,6 +3059,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3059,6 +3084,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
eventstore: expectEventstore(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3093,6 +3119,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3147,6 +3174,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3180,6 +3208,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(), // recheck of user locking relevant events
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
@@ -3203,6 +3232,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3237,9 +3267,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(),
|
||||
@@ -3311,6 +3359,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3365,6 +3414,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3429,6 +3479,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3487,6 +3538,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
tarpit: expectTarpit(0),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -3509,7 +3561,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(
|
||||
@@ -3561,15 +3613,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,
|
||||
},
|
||||
},
|
||||
@@ -3594,6 +3646,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
|
||||
userEncryption: tt.args.codeAlg,
|
||||
tarpit: tt.fields.tarpit.tarpit,
|
||||
}
|
||||
err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg)
|
||||
if tt.res.err == nil {
|
||||
@@ -3609,6 +3662,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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -284,6 +304,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
34
internal/config/systemdefaults/tarpit.go
Normal file
34
internal/config/systemdefaults/tarpit.go
Normal 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
|
||||
}
|
||||
83
internal/config/systemdefaults/tarpit_test.go
Normal file
83
internal/config/systemdefaults/tarpit_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user