mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 15:17:33 +00:00
fix: invite code generation after multiple verification failures (#10323)
<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved If a wrong verification code is used three or more times during verification, or if the verification code is expired, the user state is marked as [deleted](https://github.com/zitadel/zitadel/blob/main/internal/command/user_v2_invite_model.go#L69). This prevents the creation of a new code with the following [error](https://github.com/zitadel/zitadel/blob/main/internal/command/user_v2_invite.go#L60): `Errors.User.NotFound`. This PR aims to fix this bug. # How the Problems Are Solved This issue is solved by invalidating the previously issued invite code and setting the value of `UserV2InviteWriteModel.CodeReturned` as `false` # Additional Changes N/A # Additional Context - Closes #9860 - Follow-up: API doc update
This commit is contained in:
@@ -65,8 +65,11 @@ func (wm *UserV2InviteWriteModel) Reduce() error {
|
||||
wm.EmptyInviteCode()
|
||||
case *user.HumanInviteCheckFailedEvent:
|
||||
wm.InviteCheckFailureCount++
|
||||
if wm.InviteCheckFailureCount >= 3 { //TODO: config?
|
||||
wm.UserState = domain.UserStateDeleted
|
||||
if wm.InviteCheckFailureCount >= 3 || crypto.IsCodeExpired(wm.InviteCodeCreationDate, wm.InviteCodeExpiry) { //TODO: make failure count comparison with wm.InviteCheckFailureCount configurable?
|
||||
// invalidate the invite code after attempting to verify an expired code, or a wrong code three or more times
|
||||
// so that a new invite code can be created for this user
|
||||
wm.EmptyInviteCode()
|
||||
wm.CodeReturned = false
|
||||
}
|
||||
case *user.HumanEmailVerifiedEvent:
|
||||
wm.EmailVerified = true
|
||||
|
@@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCommands_CreateInviteCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||
@@ -323,9 +324,348 @@ func TestCommands_CreateInviteCode(t *testing.T) {
|
||||
returnCode: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"create ok after three verification failures",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName",
|
||||
"lastName",
|
||||
"nickName",
|
||||
"displayName",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
// first invite code generated and returned
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code1"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
// simulate three failed verification attempts
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code2"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
invite: &CreateUserInvite{
|
||||
UserID: "userID",
|
||||
},
|
||||
},
|
||||
want{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
ID: "userID",
|
||||
},
|
||||
returnCode: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"return ok after three verification failures",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName",
|
||||
"lastName",
|
||||
"nickName",
|
||||
"displayName",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
// first invite code generated and returned
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code1"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
// simulate three failed verification attempts
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code2"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
invite: &CreateUserInvite{
|
||||
UserID: "userID",
|
||||
ReturnCode: true,
|
||||
},
|
||||
},
|
||||
want{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
ID: "userID",
|
||||
},
|
||||
returnCode: gu.Ptr("code2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"create ok after verification fails due to invite code expiration",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName",
|
||||
"lastName",
|
||||
"nickName",
|
||||
"displayName",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
// first invite code generated and returned
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code1"),
|
||||
},
|
||||
-5*time.Minute, // expired code
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
// simulate a failed verification attempt due to expiry
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code2"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
invite: &CreateUserInvite{
|
||||
UserID: "userID",
|
||||
},
|
||||
},
|
||||
want{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
ID: "userID",
|
||||
},
|
||||
returnCode: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"return ok after verification fails due to invite code expiration",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName",
|
||||
"lastName",
|
||||
"nickName",
|
||||
"displayName",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
// first invite code generated and returned
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code1"),
|
||||
},
|
||||
-5*time.Minute, // expired code
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
// simulate a failed verification attempt due to expiry
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCheckFailedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code2"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
invite: &CreateUserInvite{
|
||||
UserID: "userID",
|
||||
ReturnCode: true,
|
||||
},
|
||||
},
|
||||
want{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
ID: "userID",
|
||||
},
|
||||
returnCode: gu.Ptr("code2"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
|
||||
@@ -342,6 +682,7 @@ func TestCommands_CreateInviteCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||
@@ -713,6 +1054,7 @@ func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
|
||||
@@ -727,6 +1069,7 @@ func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommands_InviteCodeSent(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
@@ -845,6 +1188,7 @@ func TestCommands_InviteCodeSent(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
@@ -855,6 +1199,7 @@ func TestCommands_InviteCodeSent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommands_VerifyInviteCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
@@ -940,6 +1285,7 @@ func TestCommands_VerifyInviteCode(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
@@ -952,6 +1298,7 @@ func TestCommands_VerifyInviteCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommands_VerifyInviteCodeSetPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
@@ -1268,6 +1615,7 @@ func TestCommands_VerifyInviteCodeSetPassword(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
|
Reference in New Issue
Block a user