mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:37:34 +00:00
feat: invite user link (#8578)
# Which Problems Are Solved As an administrator I want to be able to invite users to my application with the API V2, some user data I will already prefil, the user should add the authentication method themself (password, passkey, sso). # How the Problems Are Solved - A user can now be created with a email explicitly set to false. - If a user has no verified email and no authentication method, an `InviteCode` can be created through the User V2 API. - the code can be returned or sent through email - additionally `URLTemplate` and an `ApplicatioName` can provided for the email - The code can be resent and verified through the User V2 API - The V1 login allows users to verify and resend the code and set a password (analog user initialization) - The message text for the user invitation can be customized # Additional Changes - `verifyUserPasskeyCode` directly uses `crypto.VerifyCode` (instead of `verifyEncryptedCode`) - `verifyEncryptedCode` is removed (unnecessarily queried for the code generator) # Additional Context - closes #8310 - TODO: login V2 will have to implement invite flow: https://github.com/zitadel/typescript/issues/166
This commit is contained in:
@@ -41,14 +41,6 @@ func newEncryptedCodeWithDefaultConfig(ctx context.Context, filter preparation.F
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifyEncryptedCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error {
|
||||
gen, _, err := encryptedCodeGenerator(ctx, filter, typ, alg, emptyConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return crypto.VerifyCode(creation, expiry, crypted, plain, gen.Alg())
|
||||
}
|
||||
|
||||
func encryptedCodeGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) {
|
||||
config, err := cryptoGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig)
|
||||
if err != nil {
|
||||
|
@@ -123,78 +123,6 @@ func Test_newCryptoCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifyCryptoCode(t *testing.T) {
|
||||
es := eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
))
|
||||
code, err := newEncryptedCode(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) //nolint:staticcheck
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.EncryptionAlgorithm
|
||||
expiry time.Duration
|
||||
crypted *crypto.CryptoValue
|
||||
plain string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventsore *eventstore.Eventstore
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "filter config error",
|
||||
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: code.Plain,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: code.Plain,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong plain",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: "wrong",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := verifyEncryptedCode(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, time.Now(), tt.args.expiry, tt.args.crypted, tt.args.plain) //nolint:staticcheck
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cryptoCodeGenerator(t *testing.T) {
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
|
@@ -12,6 +12,9 @@ type Email struct {
|
||||
Address domain.EmailAddress
|
||||
Verified bool
|
||||
|
||||
// NoEmailVerification is used Verified field is false
|
||||
NoEmailVerification bool
|
||||
|
||||
// ReturnCode is used if the Verified field is false
|
||||
ReturnCode bool
|
||||
|
||||
|
@@ -145,6 +145,7 @@ type SecretGenerators struct {
|
||||
DomainVerification *crypto.GeneratorConfig
|
||||
OTPSMS *crypto.GeneratorConfig
|
||||
OTPEmail *crypto.GeneratorConfig
|
||||
InviteCode *crypto.GeneratorConfig
|
||||
}
|
||||
|
||||
type ZitadelConfig struct {
|
||||
|
@@ -388,6 +388,10 @@ func (c *Commands) newUserInitCode(ctx context.Context, filter preparation.Filte
|
||||
return c.newEncryptedCode(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
||||
}
|
||||
|
||||
func (c *Commands) newUserInviteCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) {
|
||||
return c.newEncryptedCodeWithDefault(ctx, filter, domain.SecretGeneratorTypeInviteCode, alg, c.defaultSecretGenerators.InviteCode)
|
||||
}
|
||||
|
||||
func userWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) (*UserWriteModel, error) {
|
||||
user := NewUserWriteModel(userID, resourceOwner)
|
||||
events, err := filter(ctx, user.Query())
|
||||
|
@@ -284,6 +284,9 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
|
||||
}
|
||||
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry, human.AuthRequestID)), nil
|
||||
}
|
||||
if human.Email.NoEmailVerification {
|
||||
return cmds, nil
|
||||
}
|
||||
if !human.Email.Verified {
|
||||
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
|
||||
if err != nil {
|
||||
|
@@ -626,6 +626,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
wantEmailCode: "emailCode",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human (with password and unverified email), ok (no email code)",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockEncryptedCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &AddHuman{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: Email{
|
||||
Address: "email@test.ch",
|
||||
Verified: false,
|
||||
NoEmailVerification: true,
|
||||
},
|
||||
PreferredLanguage: AllowedLanguage,
|
||||
},
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
allowInitMail: false,
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
wantID: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human email verified, ok",
|
||||
fields: fields{
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
@@ -122,6 +124,10 @@ func UserAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregat
|
||||
return eventstore.AggregateFromWriteModel(wm, user.AggregateType, user.AggregateVersion)
|
||||
}
|
||||
|
||||
func UserAggregateFromWriteModelCtx(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
|
||||
return eventstore.AggregateFromWriteModelCtx(ctx, wm, user.AggregateType, user.AggregateVersion)
|
||||
}
|
||||
|
||||
func isUserStateExists(state domain.UserState) bool {
|
||||
return !hasUserState(state, domain.UserStateDeleted, domain.UserStateUnspecified)
|
||||
}
|
||||
|
193
internal/command/user_v2_invite.go
Normal file
193
internal/command/user_v2_invite.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type CreateUserInvite struct {
|
||||
UserID string
|
||||
URLTemplate string
|
||||
ReturnCode bool
|
||||
ApplicationName string
|
||||
}
|
||||
|
||||
func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) {
|
||||
invite.UserID = strings.TrimSpace(invite.UserID)
|
||||
if invite.UserID == "" {
|
||||
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing")
|
||||
}
|
||||
wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !wm.UserState.Exists() {
|
||||
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound")
|
||||
}
|
||||
if !wm.CreationAllowed() {
|
||||
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised")
|
||||
}
|
||||
code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent(
|
||||
ctx,
|
||||
UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel),
|
||||
code.Crypted,
|
||||
code.Expiry,
|
||||
invite.URLTemplate,
|
||||
invite.ReturnCode,
|
||||
invite.ApplicationName,
|
||||
"",
|
||||
))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if invite.ReturnCode {
|
||||
returnCode = &code.Plain
|
||||
}
|
||||
return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil
|
||||
}
|
||||
|
||||
// ResendInviteCode resends the invite mail with a new code and an optional authRequestID.
|
||||
// It will reuse the applicationName from the previous code.
|
||||
func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
if userID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authz.GetCtxData(ctx).UserID != userID {
|
||||
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingCode.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !existingCode.UserState.Exists() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound")
|
||||
}
|
||||
if !existingCode.CreationAllowed() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised")
|
||||
}
|
||||
if existingCode.InviteCode == nil || existingCode.CodeReturned {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
|
||||
}
|
||||
code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authRequestID == "" {
|
||||
authRequestID = existingCode.AuthRequestID
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, existingCode,
|
||||
user.NewHumanInviteCodeAddedEvent(
|
||||
ctx,
|
||||
UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel),
|
||||
code.Crypted,
|
||||
code.Expiry,
|
||||
existingCode.URLTemplate,
|
||||
false,
|
||||
existingCode.ApplicationName,
|
||||
authRequestID,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) {
|
||||
if userID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing")
|
||||
}
|
||||
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !existingCode.UserState.Exists() {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-HN34a", "Errors.User.NotFound")
|
||||
}
|
||||
if existingCode.InviteCode == nil || existingCode.CodeReturned {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel)
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCodeSentEvent(ctx, userAgg))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) VerifyInviteCode(ctx context.Context, userID, code string) (details *domain.ObjectDetails, err error) {
|
||||
return c.VerifyInviteCodeSetPassword(ctx, userID, code, "", "")
|
||||
}
|
||||
|
||||
func (c *Commands) VerifyInviteCodeSetPassword(ctx context.Context, userID, code, password, userAgentID string) (details *domain.ObjectDetails, err error) {
|
||||
if userID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gk3f2", "Errors.User.UserIDMissing")
|
||||
}
|
||||
wm, err := c.userInviteCodeWriteModel(ctx, userID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !wm.UserState.Exists() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-F5g2h", "Errors.User.NotFound")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel)
|
||||
err = crypto.VerifyCode(wm.InviteCodeCreationDate, wm.InviteCodeExpiry, wm.InviteCode, code, c.userEncryption)
|
||||
if err != nil {
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCheckFailedEvent(ctx, userAgg))
|
||||
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInviteCheckFailedEvent push failed")
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Wgn4q", "Errors.User.Code.Invalid")
|
||||
}
|
||||
commands := []eventstore.Command{
|
||||
user.NewHumanInviteCheckSucceededEvent(ctx, userAgg),
|
||||
user.NewHumanEmailVerifiedEvent(ctx, userAgg),
|
||||
}
|
||||
if password != "" {
|
||||
passwordCommand, err := c.setPasswordCommand(
|
||||
ctx,
|
||||
userAgg,
|
||||
wm.UserState,
|
||||
password,
|
||||
"",
|
||||
userAgentID,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commands = append(commands, passwordCommand)
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, wm, commands...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) userInviteCodeWriteModel(ctx context.Context, userID, orgID string) (writeModel *UserV2InviteWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
writeModel = newUserV2InviteWriteModel(userID, orgID)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
141
internal/command/user_v2_invite_model.go
Normal file
141
internal/command/user_v2_invite_model.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
type UserV2InviteWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
InviteCode *crypto.CryptoValue
|
||||
InviteCodeCreationDate time.Time
|
||||
InviteCodeExpiry time.Duration
|
||||
InviteCheckFailureCount uint8
|
||||
|
||||
ApplicationName string
|
||||
AuthRequestID string
|
||||
URLTemplate string
|
||||
CodeReturned bool
|
||||
EmailVerified bool
|
||||
AuthMethodSet bool
|
||||
|
||||
UserState domain.UserState
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) CreationAllowed() bool {
|
||||
return !wm.EmailVerified && !wm.AuthMethodSet
|
||||
}
|
||||
|
||||
func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel {
|
||||
return &UserV2InviteWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: userID,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanAddedEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
|
||||
wm.EmptyInviteCode()
|
||||
wm.ApplicationName = ""
|
||||
wm.AuthRequestID = ""
|
||||
case *user.HumanRegisteredEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
|
||||
wm.EmptyInviteCode()
|
||||
wm.ApplicationName = ""
|
||||
wm.AuthRequestID = ""
|
||||
case *user.HumanInviteCodeAddedEvent:
|
||||
wm.SetInviteCode(e.Code, e.Expiry, e.CreationDate())
|
||||
wm.URLTemplate = e.URLTemplate
|
||||
wm.CodeReturned = e.CodeReturned
|
||||
wm.ApplicationName = e.ApplicationName
|
||||
wm.AuthRequestID = e.AuthRequestID
|
||||
case *user.HumanInviteCheckSucceededEvent:
|
||||
wm.EmptyInviteCode()
|
||||
case *user.HumanInviteCheckFailedEvent:
|
||||
wm.InviteCheckFailureCount++
|
||||
if wm.InviteCheckFailureCount >= 3 { //TODO: config?
|
||||
wm.UserState = domain.UserStateDeleted
|
||||
}
|
||||
case *user.HumanEmailVerifiedEvent:
|
||||
wm.EmailVerified = true
|
||||
wm.EmptyInviteCode()
|
||||
case *user.UserLockedEvent:
|
||||
wm.UserState = domain.UserStateLocked
|
||||
case *user.UserUnlockedEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.UserDeactivatedEvent:
|
||||
wm.UserState = domain.UserStateInactive
|
||||
case *user.UserReactivatedEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.UserRemovedEvent:
|
||||
wm.UserState = domain.UserStateDeleted
|
||||
case *user.HumanPasswordChangedEvent:
|
||||
wm.AuthMethodSet = true
|
||||
case *user.UserIDPLinkAddedEvent:
|
||||
wm.AuthMethodSet = true
|
||||
case *user.HumanPasswordlessVerifiedEvent:
|
||||
wm.AuthMethodSet = true
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) SetInviteCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
|
||||
wm.InviteCode = code
|
||||
wm.InviteCodeExpiry = expiry
|
||||
wm.InviteCodeCreationDate = creationDate
|
||||
wm.InviteCheckFailureCount = 0
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) EmptyInviteCode() {
|
||||
wm.InviteCode = nil
|
||||
wm.InviteCodeExpiry = 0
|
||||
wm.InviteCodeCreationDate = time.Time{}
|
||||
wm.InviteCheckFailureCount = 0
|
||||
}
|
||||
func (wm *UserV2InviteWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
user.UserV1AddedType,
|
||||
user.HumanAddedType,
|
||||
user.UserV1RegisteredType,
|
||||
user.HumanRegisteredType,
|
||||
user.HumanInviteCodeAddedType,
|
||||
user.HumanInviteCheckSucceededType,
|
||||
user.HumanInviteCheckFailedType,
|
||||
user.UserV1EmailVerifiedType,
|
||||
user.HumanEmailVerifiedType,
|
||||
user.UserLockedType,
|
||||
user.UserUnlockedType,
|
||||
user.UserDeactivatedType,
|
||||
user.UserReactivatedType,
|
||||
user.UserRemovedType,
|
||||
user.HumanPasswordChangedType,
|
||||
user.UserV1PasswordChangedType,
|
||||
user.UserIDPLinkAddedType,
|
||||
user.HumanPasswordlessTokenVerifiedType,
|
||||
).Builder()
|
||||
if wm.ResourceOwner != "" {
|
||||
query.ResourceOwner(wm.ResourceOwner)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) Aggregate() *user.Aggregate {
|
||||
return user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
}
|
1207
internal/command/user_v2_invite_test.go
Normal file
1207
internal/command/user_v2_invite_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ func (c *Commands) verifyUserPasskeyCode(ctx context.Context, userID, resourceOw
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = verifyEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg, wm.ChangeDate, wm.Expiration, wm.CryptoCode, code) //nolint:staticcheck
|
||||
err = crypto.VerifyCode(wm.ChangeDate, wm.Expiration, wm.CryptoCode, code, alg)
|
||||
if err != nil || wm.State != domain.PasswordlessInitCodeStateActive {
|
||||
c.verifyUserPasskeyCodeFailed(ctx, wm)
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid")
|
||||
|
@@ -143,7 +143,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
@@ -163,7 +163,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
{
|
||||
name: "code verification error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
@@ -174,7 +174,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectPush(
|
||||
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
@@ -192,7 +191,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
{
|
||||
name: "code verification ok, get human passwordless error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
@@ -203,7 +202,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
@@ -220,7 +218,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
@@ -242,7 +240,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
@@ -260,7 +258,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
@@ -274,7 +272,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
{
|
||||
name: "code verification error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
@@ -285,7 +283,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectPush(
|
||||
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
@@ -302,7 +299,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
@@ -313,7 +310,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
@@ -328,7 +324,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := c.verifyUserPasskeyCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.codeID, tt.args.code, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
|
Reference in New Issue
Block a user