+
+
+
-
+
+
{{ if .MFAProviders }}
@@ -45,4 +48,4 @@
-{{template "main-bottom" .}}
\ No newline at end of file
+{{template "main-bottom" .}}
diff --git a/internal/api/ui/login/static/templates/mfa_verify_totp.html b/internal/api/ui/login/static/templates/mfa_verify_totp.html
new file mode 100644
index 000000000..345391660
--- /dev/null
+++ b/internal/api/ui/login/static/templates/mfa_verify_totp.html
@@ -0,0 +1,48 @@
+{{template "main-top" .}}
+
+
+
{{t "VerifyMFAOTP.Title"}}
+
+ {{ template "user-profile" . }}
+
+
{{t "VerifyMFAOTP.Description"}}
+
+
+
+
+
+
+{{template "main-bottom" .}}
\ No newline at end of file
diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go
index 8851b4f67..db2270505 100644
--- a/internal/auth/repository/auth_request.go
+++ b/internal/auth/repository/auth_request.go
@@ -23,6 +23,10 @@ type AuthRequestRepository interface {
VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error
VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error
+ SendMFAOTPSMS(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) error
+ VerifyMFAOTPSMS(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) error
+ SendMFAOTPEmail(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) error
+ VerifyMFAOTPEmail(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) error
BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
VerifyMFAU2F(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string, preferredPlatformType domain.AuthenticatorAttachment) (login *domain.WebAuthNToken, err error)
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
index 80be2a8e3..0bfa0fe82 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
@@ -376,6 +376,48 @@ func (repo *AuthRequestRepo) VerifyMFAOTP(ctx context.Context, authRequestID, us
return repo.Command.HumanCheckMFATOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
}
+func (repo *AuthRequestRepo) SendMFAOTPSMS(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
+ if err != nil {
+ return err
+ }
+ return repo.Command.HumanSendOTPSMS(ctx, userID, resourceOwner, request)
+}
+
+func (repo *AuthRequestRepo) VerifyMFAOTPSMS(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+ request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
+ if err != nil {
+ return err
+ }
+ return repo.Command.HumanCheckOTPSMS(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
+}
+
+func (repo *AuthRequestRepo) SendMFAOTPEmail(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
+ if err != nil {
+ return err
+ }
+ return repo.Command.HumanSendOTPEmail(ctx, userID, resourceOwner, request)
+}
+
+func (repo *AuthRequestRepo) VerifyMFAOTPEmail(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+ request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
+ if err != nil {
+ return err
+ }
+ return repo.Command.HumanCheckOTPEmail(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
+}
+
func (repo *AuthRequestRepo) BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
diff --git a/internal/auth/repository/eventsourcing/handler/user.go b/internal/auth/repository/eventsourcing/handler/user.go
index 847f483ea..4224a0ca6 100644
--- a/internal/auth/repository/eventsourcing/handler/user.go
+++ b/internal/auth/repository/eventsourcing/handler/user.go
@@ -138,6 +138,10 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
user_repo.HumanMFAOTPAddedType,
user_repo.HumanMFAOTPVerifiedType,
user_repo.HumanMFAOTPRemovedType,
+ user_repo.HumanOTPSMSAddedType,
+ user_repo.HumanOTPSMSRemovedType,
+ user_repo.HumanOTPEmailAddedType,
+ user_repo.HumanOTPEmailRemovedType,
user_repo.HumanU2FTokenAddedType,
user_repo.HumanU2FTokenVerifiedType,
user_repo.HumanU2FTokenRemovedType,
diff --git a/internal/command/user_converter.go b/internal/command/user_converter.go
index 7febe6853..076b0bfd5 100644
--- a/internal/command/user_converter.go
+++ b/internal/command/user_converter.go
@@ -137,6 +137,9 @@ func writeModelToWebAuthN(wm *HumanWebAuthNWriteModel) *domain.WebAuthNToken {
}
func authRequestDomainToAuthRequestInfo(authRequest *domain.AuthRequest) *user.AuthRequestInfo {
+ if authRequest == nil {
+ return nil
+ }
info := &user.AuthRequestInfo{
ID: authRequest.ID,
UserAgentID: authRequest.AgentID,
diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go
index a1f207b40..c3a8e84f0 100644
--- a/internal/command/user_human_otp.go
+++ b/internal/command/user_human_otp.go
@@ -2,6 +2,7 @@ package command
import (
"context"
+ "time"
"github.com/pquerna/otp"
"github.com/zitadel/logging"
@@ -15,16 +16,16 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
-func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
+func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, resourceOwner string, key string) error {
encryptedSecret, err := crypto.Encrypt([]byte(key), c.multifactors.OTP.CryptoMFA)
if err != nil {
return err
}
- if err = c.checkUserExists(ctx, userID, resourceowner); err != nil {
+ if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}
- otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceowner)
+ otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
@@ -40,11 +41,11 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res
return err
}
-func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) {
+func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceOwner string) (*domain.TOTP, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
- prep, err := c.createHumanTOTP(ctx, userID, resourceowner)
+ prep, err := c.createHumanTOTP(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@@ -114,12 +115,12 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
}, nil
}
-func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
+func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, userAgentID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
- existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
+ existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@@ -145,11 +146,11 @@ func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, use
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
-func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
+func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
- existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
+ existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
@@ -191,7 +192,26 @@ func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner st
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
+// AddHumanOTPSMS adds the OTP SMS factor to a user.
+// It can only be added if it not already is and the phone has to be verified.
func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
+ return c.addHumanOTPSMS(ctx, userID, resourceOwner)
+}
+
+// AddHumanOTPSMSWithCheckSucceeded adds the OTP SMS factor to a user.
+// It can only be added if it's not already and the phone has to be verified.
+// An OTPSMSCheckSucceededEvent will be added to the passed AuthRequest, if not nil.
+func (c *Commands) AddHumanOTPSMSWithCheckSucceeded(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) (*domain.ObjectDetails, error) {
+ if authRequest == nil {
+ return c.addHumanOTPSMS(ctx, userID, resourceOwner)
+ }
+ event := func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
+ return user.NewHumanOTPSMSCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ return c.addHumanOTPSMS(ctx, userID, resourceOwner, event)
+}
+
+func (c *Commands) addHumanOTPSMS(ctx context.Context, userID, resourceOwner string, events ...eventCallback) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing")
}
@@ -209,7 +229,12 @@ func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner str
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
- if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPSMSAddedEvent(ctx, userAgg)); err != nil {
+ cmds := make([]eventstore.Command, len(events)+1)
+ cmds[0] = user.NewHumanOTPSMSAddedEvent(ctx, userAgg)
+ for i, event := range events {
+ cmds[i+1] = event(ctx, userAgg)
+ }
+ if err = c.pushAppendAndReduce(ctx, otpWriteModel, cmds...); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
@@ -225,7 +250,7 @@ func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
- if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
+ if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.WriteModel.ResourceOwner, userID); err != nil {
return nil, err
}
}
@@ -239,7 +264,77 @@ func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
+func (c *Commands) HumanSendOTPSMS(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) error {
+ smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
+ return c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
+ }
+ codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPSMSCodeAddedEvent(ctx, aggregate, code, expiry, info)
+ }
+ return c.sendHumanOTP(
+ ctx,
+ userID,
+ resourceOwner,
+ authRequest,
+ smsWriteModel,
+ domain.SecretGeneratorTypeOTPSMS,
+ codeAddedEvent,
+ )
+}
+
+func (c *Commands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string) (err error) {
+ smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
+ return c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
+ }
+ codeSentEvent := func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command {
+ return user.NewHumanOTPSMSCodeSentEvent(ctx, aggregate)
+ }
+ return c.humanOTPSent(ctx, userID, resourceOwner, smsWriteModel, codeSentEvent)
+}
+
+func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
+ writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
+ return c.otpSMSCodeWriteModelByID(ctx, userID, resourceOwner)
+ }
+ succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPSMSCheckSucceededEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ return c.humanCheckOTP(
+ ctx,
+ userID,
+ code,
+ resourceOwner,
+ authRequest,
+ writeModel,
+ domain.SecretGeneratorTypeOTPSMS,
+ succeededEvent,
+ failedEvent,
+ )
+}
+
+// AddHumanOTPEmail adds the OTP Email factor to a user.
+// It can only be added if it not already is and the phone has to be verified.
func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
+ return c.addHumanOTPEmail(ctx, userID, resourceOwner)
+}
+
+// AddHumanOTPEmailWithCheckSucceeded adds the OTP Email factor to a user.
+// It can only be added if it's not already and the email has to be verified.
+// An OTPEmailCheckSucceededEvent will be added to the passed AuthRequest, if not nil.
+func (c *Commands) AddHumanOTPEmailWithCheckSucceeded(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) (*domain.ObjectDetails, error) {
+ if authRequest == nil {
+ return c.addHumanOTPEmail(ctx, userID, resourceOwner)
+ }
+ event := func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
+ return user.NewHumanOTPEmailCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ return c.addHumanOTPEmail(ctx, userID, resourceOwner, event)
+}
+
+func (c *Commands) addHumanOTPEmail(ctx context.Context, userID, resourceOwner string, events ...eventCallback) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing")
}
@@ -254,7 +349,12 @@ func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner s
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
- if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPEmailAddedEvent(ctx, userAgg)); err != nil {
+ cmds := make([]eventstore.Command, len(events)+1)
+ cmds[0] = user.NewHumanOTPEmailAddedEvent(ctx, userAgg)
+ for i, event := range events {
+ cmds[i+1] = event(ctx, userAgg)
+ }
+ if err = c.pushAppendAndReduce(ctx, otpWriteModel, cmds...); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
@@ -270,7 +370,7 @@ func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwne
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
- if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
+ if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.WriteModel.ResourceOwner, userID); err != nil {
return nil, err
}
}
@@ -284,6 +384,147 @@ func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwne
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
+func (c *Commands) HumanSendOTPEmail(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) error {
+ smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
+ return c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
+ }
+ codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPEmailCodeAddedEvent(ctx, aggregate, code, expiry, info)
+ }
+ return c.sendHumanOTP(
+ ctx,
+ userID,
+ resourceOwner,
+ authRequest,
+ smsWriteModel,
+ domain.SecretGeneratorTypeOTPEmail,
+ codeAddedEvent,
+ )
+}
+
+func (c *Commands) HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) (err error) {
+ smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
+ return c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
+ }
+ codeSentEvent := func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command {
+ return user.NewHumanOTPEmailCodeSentEvent(ctx, aggregate)
+ }
+ return c.humanOTPSent(ctx, userID, resourceOwner, smsWriteModel, codeSentEvent)
+}
+
+func (c *Commands) HumanCheckOTPEmail(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
+ writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
+ return c.otpEmailCodeWriteModelByID(ctx, userID, resourceOwner)
+ }
+ succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPEmailCheckSucceededEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
+ }
+ return c.humanCheckOTP(
+ ctx,
+ userID,
+ code,
+ resourceOwner,
+ authRequest,
+ writeModel,
+ domain.SecretGeneratorTypeOTPEmail,
+ succeededEvent,
+ failedEvent,
+ )
+}
+
+// sendHumanOTP creates a code for a registered mechanism (sms / email), which is used for a check (during login)
+func (c *Commands) sendHumanOTP(
+ ctx context.Context,
+ userID, resourceOwner string,
+ authRequest *domain.AuthRequest,
+ writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error),
+ secretGeneratorType domain.SecretGeneratorType,
+ codeAddedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command,
+) (err error) {
+ if userID == "" {
+ return caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing")
+ }
+ existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
+ if err != nil {
+ return err
+ }
+ if !existingOTP.OTPAdded() {
+ return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady")
+ }
+ config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, secretGeneratorType)
+ if err != nil {
+ return err
+ }
+ gen := crypto.NewEncryptionGenerator(*config, c.userEncryption)
+ value, _, err := crypto.NewCode(gen)
+ if err != nil {
+ return err
+ }
+ userAgg := &user.NewAggregate(userID, resourceOwner).Aggregate
+ _, err = c.eventstore.Push(ctx, codeAddedEvent(ctx, userAgg, value, gen.Expiry(), authRequestDomainToAuthRequestInfo(authRequest)))
+ return err
+}
+
+func (c *Commands) humanOTPSent(
+ ctx context.Context,
+ userID, resourceOwner string,
+ writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error),
+ codeSentEvent func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command,
+) (err error) {
+ if userID == "" {
+ return caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing")
+ }
+ existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
+ if err != nil {
+ return err
+ }
+ if !existingOTP.OTPAdded() {
+ return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady")
+ }
+ userAgg := &user.NewAggregate(userID, resourceOwner).Aggregate
+ _, err = c.eventstore.Push(ctx, codeSentEvent(ctx, userAgg))
+ return err
+}
+
+func (c *Commands) humanCheckOTP(
+ ctx context.Context,
+ userID, code, resourceOwner string,
+ authRequest *domain.AuthRequest,
+ writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error),
+ secretGeneratorType domain.SecretGeneratorType,
+ checkSucceededEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
+ checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
+) error {
+ if userID == "" {
+ return caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing")
+ }
+ if code == "" {
+ return caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty")
+ }
+ existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
+ if err != nil {
+ return err
+ }
+ if !existingOTP.OTPAdded() {
+ return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady")
+ }
+ if existingOTP.Code() == nil {
+ return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound")
+ }
+ userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate
+ err = crypto.VerifyCodeWithAlgorithm(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, c.userEncryption)
+ if err == nil {
+ _, err = c.eventstore.Push(ctx, checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+ return err
+ }
+ _, pushErr := c.eventstore.Push(ctx, checkFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+ logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
+ return err
+}
+
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -308,6 +549,18 @@ func (c *Commands) otpSMSWriteModelByID(ctx context.Context, userID, resourceOwn
return writeModel, nil
}
+func (c *Commands) otpSMSCodeWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPSMSCodeWriteModel, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ writeModel = NewHumanOTPSMSCodeWriteModel(userID, resourceOwner)
+ err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ if err != nil {
+ return nil, err
+ }
+ return writeModel, nil
+}
+
func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -319,3 +572,15 @@ func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceO
}
return writeModel, nil
}
+
+func (c *Commands) otpEmailCodeWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailCodeWriteModel, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ writeModel = NewHumanOTPEmailCodeWriteModel(userID, resourceOwner)
+ err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ if err != nil {
+ return nil, err
+ }
+ return writeModel, nil
+}
diff --git a/internal/command/user_human_otp_model.go b/internal/command/user_human_otp_model.go
index 9c9b8d2c4..25bd4c9ef 100644
--- a/internal/command/user_human_otp_model.go
+++ b/internal/command/user_human_otp_model.go
@@ -1,6 +1,8 @@
package command
import (
+ "time"
+
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -60,6 +62,18 @@ func (wm *HumanTOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
return query
}
+type OTPWriteModel interface {
+ OTPAdded() bool
+ ResourceOwner() string
+}
+
+type OTPCodeWriteModel interface {
+ OTPWriteModel
+ CodeCreationDate() time.Time
+ CodeExpiry() time.Duration
+ Code() *crypto.CryptoValue
+}
+
type HumanOTPSMSWriteModel struct {
eventstore.WriteModel
@@ -67,6 +81,14 @@ type HumanOTPSMSWriteModel struct {
otpAdded bool
}
+func (wm *HumanOTPSMSWriteModel) OTPAdded() bool {
+ return wm.otpAdded
+}
+
+func (wm *HumanOTPSMSWriteModel) ResourceOwner() string {
+ return wm.WriteModel.ResourceOwner
+}
+
func NewHumanOTPSMSWriteModel(userID, resourceOwner string) *HumanOTPSMSWriteModel {
return &HumanOTPSMSWriteModel{
WriteModel: eventstore.WriteModel{
@@ -107,8 +129,66 @@ func (wm *HumanOTPSMSWriteModel) Query() *eventstore.SearchQueryBuilder {
).
Builder()
- if wm.ResourceOwner != "" {
- query.ResourceOwner(wm.ResourceOwner)
+ if wm.WriteModel.ResourceOwner != "" {
+ query.ResourceOwner(wm.WriteModel.ResourceOwner)
+ }
+ return query
+}
+
+type HumanOTPSMSCodeWriteModel struct {
+ *HumanOTPSMSWriteModel
+
+ code *crypto.CryptoValue
+ codeCreationDate time.Time
+ codeExpiry time.Duration
+}
+
+func (wm *HumanOTPSMSCodeWriteModel) CodeCreationDate() time.Time {
+ return wm.codeCreationDate
+}
+
+func (wm *HumanOTPSMSCodeWriteModel) CodeExpiry() time.Duration {
+ return wm.codeExpiry
+}
+
+func (wm *HumanOTPSMSCodeWriteModel) Code() *crypto.CryptoValue {
+ return wm.code
+}
+
+func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCodeWriteModel {
+ return &HumanOTPSMSCodeWriteModel{
+ HumanOTPSMSWriteModel: NewHumanOTPSMSWriteModel(userID, resourceOwner),
+ }
+}
+
+func (wm *HumanOTPSMSCodeWriteModel) Reduce() error {
+ for _, event := range wm.Events {
+ if e, ok := event.(*user.HumanOTPSMSCodeAddedEvent); ok {
+ wm.code = e.Code
+ wm.codeCreationDate = e.CreationDate()
+ wm.codeExpiry = e.Expiry
+ }
+ }
+ return wm.HumanOTPSMSWriteModel.Reduce()
+}
+
+func (wm *HumanOTPSMSCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
+ query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AddQuery().
+ AggregateTypes(user.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ user.HumanOTPSMSCodeAddedType,
+ user.HumanPhoneVerifiedType,
+ user.HumanOTPSMSAddedType,
+ user.HumanOTPSMSRemovedType,
+ user.HumanPhoneRemovedType,
+ user.UserRemovedType,
+ ).
+ Builder()
+
+ if wm.WriteModel.ResourceOwner != "" {
+ query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}
@@ -120,6 +200,14 @@ type HumanOTPEmailWriteModel struct {
otpAdded bool
}
+func (wm *HumanOTPEmailWriteModel) OTPAdded() bool {
+ return wm.otpAdded
+}
+
+func (wm *HumanOTPEmailWriteModel) ResourceOwner() string {
+ return wm.WriteModel.ResourceOwner
+}
+
func NewHumanOTPEmailWriteModel(userID, resourceOwner string) *HumanOTPEmailWriteModel {
return &HumanOTPEmailWriteModel{
WriteModel: eventstore.WriteModel{
@@ -151,15 +239,73 @@ func (wm *HumanOTPEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
- EventTypes(user.HumanEmailVerifiedType,
+ EventTypes(
+ user.HumanEmailVerifiedType,
user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType,
user.UserRemovedType,
).
Builder()
- if wm.ResourceOwner != "" {
- query.ResourceOwner(wm.ResourceOwner)
+ if wm.WriteModel.ResourceOwner != "" {
+ query.ResourceOwner(wm.WriteModel.ResourceOwner)
+ }
+ return query
+}
+
+type HumanOTPEmailCodeWriteModel struct {
+ *HumanOTPEmailWriteModel
+
+ code *crypto.CryptoValue
+ codeCreationDate time.Time
+ codeExpiry time.Duration
+}
+
+func (wm *HumanOTPEmailCodeWriteModel) CodeCreationDate() time.Time {
+ return wm.codeCreationDate
+}
+
+func (wm *HumanOTPEmailCodeWriteModel) CodeExpiry() time.Duration {
+ return wm.codeExpiry
+}
+
+func (wm *HumanOTPEmailCodeWriteModel) Code() *crypto.CryptoValue {
+ return wm.code
+}
+
+func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmailCodeWriteModel {
+ return &HumanOTPEmailCodeWriteModel{
+ HumanOTPEmailWriteModel: NewHumanOTPEmailWriteModel(userID, resourceOwner),
+ }
+}
+
+func (wm *HumanOTPEmailCodeWriteModel) Reduce() error {
+ for _, event := range wm.Events {
+ if e, ok := event.(*user.HumanOTPEmailCodeAddedEvent); ok {
+ wm.code = e.Code
+ wm.codeCreationDate = e.CreationDate()
+ wm.codeExpiry = e.Expiry
+ }
+ }
+ return wm.HumanOTPEmailWriteModel.Reduce()
+}
+
+func (wm *HumanOTPEmailCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
+ query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AddQuery().
+ AggregateTypes(user.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ user.HumanOTPEmailCodeAddedType,
+ user.HumanEmailVerifiedType,
+ user.HumanOTPEmailAddedType,
+ user.HumanOTPEmailRemovedType,
+ user.UserRemovedType,
+ ).
+ Builder()
+
+ if wm.WriteModel.ResourceOwner != "" {
+ query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}
diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go
index 28e825de4..5ec52582a 100644
--- a/internal/command/user_human_otp_test.go
+++ b/internal/command/user_human_otp_test.go
@@ -3,6 +3,7 @@ package command
import (
"context"
"io"
+ "net"
"testing"
"time"
@@ -18,6 +19,7 @@ import (
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -989,6 +991,143 @@ func TestCommandSide_AddHumanOTPSMS(t *testing.T) {
}
}
+func TestCommandSide_AddHumanOTPSMSWithCheckSucceeded(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanPhoneChangedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "+4179654321",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPhoneVerifiedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ {
+ name: "successful add with auth request",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanPhoneChangedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "+4179654321",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPhoneVerifiedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCheckSucceededEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ }
+ got, err := r.AddHumanOTPSMSWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest)
+ assert.ErrorIs(t, err, tt.res.err)
+ assert.Equal(t, tt.res.want, got)
+ })
+ }
+}
+
func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
@@ -1108,6 +1247,551 @@ func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) {
}
}
+func TestCommandSide_HumanSendOTPSMS(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "otp sms not added, not found error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewSecretGeneratorAddedEvent(context.Background(),
+ &instance.NewAggregate("instanceID").Aggregate,
+ domain.SecretGeneratorTypeOTPSMS,
+ 8,
+ time.Hour,
+ true,
+ true,
+ true,
+ true,
+ )),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("12345678"),
+ },
+ time.Hour,
+ nil,
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ {
+ name: "successful add with auth request",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewSecretGeneratorAddedEvent(context.Background(),
+ &instance.NewAggregate("instanceID").Aggregate,
+ domain.SecretGeneratorTypeOTPSMS,
+ 8,
+ time.Hour,
+ true,
+ true,
+ true,
+ true,
+ )),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("12345678"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ userEncryption: tt.fields.userEncryption,
+ }
+ err := r.HumanSendOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommandSide_HumanOTPSMSCodeSent(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "otp sms not added, not found error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCodeSentEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ }
+ err := r.HumanOTPSMSCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ code string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ code: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "code missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
+ },
+ },
+ {
+ name: "otp sms not added, precondition failed error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "otp sms code not added, precondition failed error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
+ },
+ },
+ {
+ name: "invalid code, error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ user.NewHumanOTPSMSCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("other-code"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCheckFailedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
+ },
+ },
+ {
+ name: "code ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPSMSAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ user.NewHumanOTPSMSCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPSMSCheckSucceededEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ userEncryption: tt.fields.userEncryption,
+ }
+ 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)
+ })
+ }
+}
+
func TestCommandSide_AddHumanOTPEmail(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
@@ -1234,6 +1918,143 @@ func TestCommandSide_AddHumanOTPEmail(t *testing.T) {
}
}
+func TestCommandSide_AddHumanOTPEmailWithCheckSucceeded(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanEmailChangedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "email@test.ch",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ {
+ name: "successful add with auth request",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanEmailChangedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "email@test.ch",
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCheckSucceededEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ }
+ got, err := r.AddHumanOTPEmailWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest)
+ assert.ErrorIs(t, err, tt.res.err)
+ assert.Equal(t, tt.res.want, got)
+ })
+ }
+}
+
func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) {
ctx := authz.NewMockContext("inst1", "org1", "user1")
type fields struct {
@@ -1352,3 +2173,548 @@ func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) {
})
}
}
+
+func TestCommandSide_HumanSendOTPEmail(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "otp email not added, not found error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewSecretGeneratorAddedEvent(context.Background(),
+ &instance.NewAggregate("instanceID").Aggregate,
+ domain.SecretGeneratorTypeOTPEmail,
+ 8,
+ time.Hour,
+ true,
+ true,
+ true,
+ true,
+ )),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("12345678"),
+ },
+ time.Hour,
+ nil,
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ {
+ name: "successful add with auth request",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewSecretGeneratorAddedEvent(context.Background(),
+ &instance.NewAggregate("instanceID").Aggregate,
+ domain.SecretGeneratorTypeOTPEmail,
+ 8,
+ time.Hour,
+ true,
+ true,
+ true,
+ true,
+ )),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("12345678"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ userEncryption: tt.fields.userEncryption,
+ }
+ err := r.HumanSendOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommandSide_HumanOTPEmailCodeSent(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ resourceOwner string
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "otp email not added, not found error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "successful add",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCodeSentEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ resourceOwner: "org1",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ }
+ err := r.HumanOTPEmailCodeSent(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
+ ctx := authz.NewMockContext("inst1", "org1", "user1")
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
+ }
+ type (
+ args struct {
+ ctx context.Context
+ userID string
+ code string
+ resourceOwner string
+ authRequest *domain.AuthRequest
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "userid missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "",
+ code: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "code missing, invalid argument error",
+ fields: fields{
+ eventstore: expectEventstore(),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
+ },
+ },
+ {
+ name: "otp email not added, precondition failed error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
+ },
+ },
+ {
+ name: "otp email code not added, precondition failed error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ ),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ },
+ res: res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
+ },
+ },
+ {
+ name: "invalid code, error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ user.NewHumanOTPEmailCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("other-code"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCheckFailedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ err: caos_errs.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
+ },
+ },
+ {
+ name: "code ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPEmailAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ user.NewHumanOTPEmailCodeAddedEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ time.Hour,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("inst1",
+ user.NewHumanOTPEmailCheckSucceededEvent(ctx,
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "authRequestID",
+ UserAgentID: "userAgentID",
+ BrowserInfo: &user.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ ),
+ ),
+ },
+ ),
+ ),
+ userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: ctx,
+ userID: "user1",
+ code: "code",
+ resourceOwner: "org1",
+ authRequest: &domain.AuthRequest{
+ ID: "authRequestID",
+ AgentID: "userAgentID",
+ BrowserInfo: &domain.BrowserInfo{
+ UserAgent: "user-agent",
+ AcceptLanguage: "en",
+ RemoteIP: net.IP{192, 0, 2, 1},
+ },
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ userEncryption: tt.fields.userEncryption,
+ }
+ 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)
+ })
+ }
+}
diff --git a/internal/command/user_human_phone.go b/internal/command/user_human_phone.go
index 525333957..43395acd1 100644
--- a/internal/command/user_human_phone.go
+++ b/internal/command/user_human_phone.go
@@ -97,7 +97,7 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo
return nil, caos_errs.ThrowInvalidArgument(err, "COMMAND-sM0cs", "Errors.User.Code.Invalid")
}
-func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string, phoneCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
+func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
@@ -116,19 +116,17 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID,
if existingPhone.IsPhoneVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sf", "Errors.User.Phone.AlreadyVerified")
}
-
- phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator)
+ config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode)
+ if err != nil {
+ return nil, err
+ }
+ phoneCode, err := domain.NewPhoneCode(crypto.NewEncryptionGenerator(*config, c.userEncryption))
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel)
- pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry))
- if err != nil {
- return nil, err
- }
- err = AppendAndReduce(existingPhone, pushedEvents...)
- if err != nil {
+ if err = c.pushAppendAndReduce(ctx, existingPhone, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingPhone.WriteModel), nil
diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go
index 98ee9da7c..8a2b426cb 100644
--- a/internal/command/user_human_phone_test.go
+++ b/internal/command/user_human_phone_test.go
@@ -5,6 +5,7 @@ import (
"testing"
"time"
+ "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
@@ -14,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -584,13 +586,13 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) {
func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
+ eventstore *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
}
type args struct {
- ctx context.Context
- userID string
- resourceOwner string
- secretGenerator crypto.Generator
+ ctx context.Context
+ userID string
+ resourceOwner string
}
type res struct {
want *domain.ObjectDetails
@@ -704,6 +706,19 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
),
),
),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewSecretGeneratorAddedEvent(context.Background(),
+ &instance.NewAggregate("instanceID").Aggregate,
+ domain.SecretGeneratorTypeVerifyPhoneCode,
+ 8,
+ time.Hour,
+ true,
+ true,
+ true,
+ true,
+ )),
+ ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -713,7 +728,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte("a"),
+ Crypted: []byte("12345678"),
},
time.Hour*1,
),
@@ -721,12 +736,12 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
},
),
),
+ userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
},
args: args{
- ctx: context.Background(),
- userID: "user1",
- resourceOwner: "org1",
- secretGenerator: GetMockSecretGenerator(t),
+ ctx: context.Background(),
+ userID: "user1",
+ resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
@@ -738,9 +753,10 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
+ eventstore: tt.fields.eventstore,
+ userEncryption: tt.fields.userEncryption,
}
- got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator)
+ got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
diff --git a/internal/crypto/code_mocker.go b/internal/crypto/code_mocker.go
index 181ec771d..b29940181 100644
--- a/internal/crypto/code_mocker.go
+++ b/internal/crypto/code_mocker.go
@@ -9,14 +9,37 @@ import (
)
func CreateMockEncryptionAlg(ctrl *gomock.Controller) EncryptionAlgorithm {
+ return createMockEncryptionAlgorithm(
+ ctrl,
+ func(code []byte) ([]byte, error) {
+ return code, nil
+ },
+ )
+}
+
+// CreateMockEncryptionAlgWithCode compares the length of the value to be encrypted with the length of the provided code.
+// It will return an error if they do not match.
+// The provided code will be used to encrypt in favor of the value passed to the encryption.
+// This function is intended to be used where the passed value is not in control, but where the returned encryption requires a static value.
+func CreateMockEncryptionAlgWithCode(ctrl *gomock.Controller, code string) EncryptionAlgorithm {
+ return createMockEncryptionAlgorithm(
+ ctrl,
+ func(c []byte) ([]byte, error) {
+ if len(c) != len(code) {
+ return nil, errors.ThrowInvalidArgumentf(nil, "id", "invalid code length - expected %d, got %d", len(code), len(c))
+ }
+ return []byte(code), nil
+ },
+ )
+}
+
+func createMockEncryptionAlgorithm(ctrl *gomock.Controller, encryptFunction func(c []byte) ([]byte, error)) *MockEncryptionAlgorithm {
mCrypto := NewMockEncryptionAlgorithm(ctrl)
mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc")
mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id")
mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"})
mCrypto.EXPECT().Encrypt(gomock.Any()).AnyTimes().DoAndReturn(
- func(code []byte) ([]byte, error) {
- return code, nil
- },
+ encryptFunction,
)
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
func(code []byte, keyID string) (string, error) {
diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go
index f7f4da19f..d5b1bf4fe 100644
--- a/internal/domain/auth_request.go
+++ b/internal/domain/auth_request.go
@@ -105,6 +105,8 @@ const (
MFATypeTOTP MFAType = iota
MFATypeU2F
MFATypeU2FUserVerification
+ MFATypeOTPSMS
+ MFATypeOTPEmail
)
type MFALevel int
diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go
index 64661d89b..d5ad60b46 100644
--- a/internal/notification/handlers/user_notifier.go
+++ b/internal/notification/handlers/user_notifier.go
@@ -4,6 +4,7 @@ import (
"context"
"time"
+ "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -106,6 +107,14 @@ func (u *userNotifier) reducers() []handler.AggregateReducer {
Event: user.HumanPasswordChangedType,
Reduce: u.reducePasswordChanged,
},
+ {
+ Event: user.HumanOTPSMSCodeAddedType,
+ Reduce: u.reduceOTPSMSCodeAdded,
+ },
+ {
+ Event: user.HumanOTPEmailCodeAddedType,
+ Reduce: u.reduceOTPEmailCodeAdded,
+ },
},
},
}
@@ -332,6 +341,136 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler
return crdb.NewNoOpStatement(e), nil
}
+func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*user.HumanOTPSMSCodeAddedEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType)
+ }
+ ctx := HandlerContext(event.Aggregate())
+ alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
+ user.HumanOTPSMSCodeAddedType, user.HumanOTPSMSCodeSentType)
+ if err != nil {
+ return nil, err
+ }
+ if alreadyHandled {
+ return crdb.NewNoOpStatement(e), nil
+ }
+ code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
+ if err != nil {
+ return nil, err
+ }
+ colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
+ if err != nil {
+ return nil, err
+ }
+
+ notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
+ if err != nil {
+ return nil, err
+ }
+ translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifySMSOTPMessageType)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx, origin, err := u.queries.Origin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ notify := types.SendSMSTwilio(
+ ctx,
+ translator,
+ notifyUser,
+ u.queries.GetTwilioConfig,
+ u.queries.GetFileSystemProvider,
+ u.queries.GetLogProvider,
+ colors,
+ u.assetsPrefix(ctx),
+ e,
+ u.metricSuccessfulDeliveriesSMS,
+ u.metricFailedDeliveriesSMS,
+ )
+ err = notify.SendOTPSMSCode(authz.GetInstance(ctx).RequestedDomain(), origin, code, e.Expiry)
+ if err != nil {
+ return nil, err
+ }
+ err = u.commands.HumanOTPSMSCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
+ if err != nil {
+ return nil, err
+ }
+ return crdb.NewNoOpStatement(e), nil
+}
+
+func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*user.HumanOTPEmailCodeAddedEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType)
+ }
+ ctx := HandlerContext(event.Aggregate())
+ alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
+ user.HumanOTPEmailCodeAddedType, user.HumanOTPEmailCodeSentType)
+ if err != nil {
+ return nil, err
+ }
+ if alreadyHandled {
+ return crdb.NewNoOpStatement(e), nil
+ }
+ code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
+ if err != nil {
+ return nil, err
+ }
+ colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
+ if err != nil {
+ return nil, err
+ }
+
+ template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
+ if err != nil {
+ return nil, err
+ }
+
+ notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
+ if err != nil {
+ return nil, err
+ }
+ translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailOTPMessageType)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx, origin, err := u.queries.Origin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var authRequestID string
+ if e.AuthRequestInfo != nil {
+ authRequestID = e.AuthRequestInfo.ID
+ }
+ notify := types.SendEmail(
+ ctx,
+ string(template.Template),
+ translator,
+ notifyUser,
+ u.queries.GetSMTPConfig,
+ u.queries.GetFileSystemProvider,
+ u.queries.GetLogProvider,
+ colors,
+ u.assetsPrefix(ctx),
+ e,
+ u.metricSuccessfulDeliveriesEmail,
+ u.metricFailedDeliveriesEmail,
+ )
+ err = notify.SendOTPEmailCode(notifyUser, authz.GetInstance(ctx).RequestedDomain(), origin, code, authRequestID, e.Expiry)
+ if err != nil {
+ return nil, err
+ }
+ err = u.commands.HumanOTPEmailCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
+ if err != nil {
+ return nil, err
+ }
+ return crdb.NewNoOpStatement(e), nil
+}
+
func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.DomainClaimedEvent)
if !ok {
@@ -583,7 +722,7 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
e,
u.metricSuccessfulDeliveriesSMS,
u.metricFailedDeliveriesSMS,
- ).SendPhoneVerificationCode(notifyUser, origin, code)
+ ).SendPhoneVerificationCode(notifyUser, origin, code, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
diff --git a/internal/notification/static/i18n/bg.yaml b/internal/notification/static/i18n/bg.yaml
index 127163e38..7f67c24df 100644
--- a/internal/notification/static/i18n/bg.yaml
+++ b/internal/notification/static/i18n/bg.yaml
@@ -40,7 +40,10 @@ VerifyEmailOTP:
Text: Моля, използвай бутона 'Удостовери' или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
ButtonText: Удостовери
VerifySMSOTP:
- Text: Моля, посети {{ .VerifyURL }} или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
+ Text: >-
+ {{.OTP}} е вашата еднократна парола за {{ .Domain }}. Използвайте го в рамките на следващия {{.Expiry}}.
+
+ @{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Домейнът е заявен
PreHeader: Промяна на имейл/потребителско име
diff --git a/internal/notification/static/i18n/de.yaml b/internal/notification/static/i18n/de.yaml
index c882f2b79..bf3fe133c 100644
--- a/internal/notification/static/i18n/de.yaml
+++ b/internal/notification/static/i18n/de.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Bitte nutze den 'Authentifizieren'-Button oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
ButtonText: Authentifizieren
VerifySMSOTP:
- Text: Bitte besuche {{ .VerifyURL }} oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
+ Text: >-
+ {{.OTP}} ist Ihr Einmalpasswort für {{ .Domain }}. Verwenden Sie es innerhalb der nächsten {{.Expiry}}.
+
+ @{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domain wurde beansprucht
PreHeader: Email / Username ändern
diff --git a/internal/notification/static/i18n/en.yaml b/internal/notification/static/i18n/en.yaml
index bb2a841ad..c6257188e 100644
--- a/internal/notification/static/i18n/en.yaml
+++ b/internal/notification/static/i18n/en.yaml
@@ -31,10 +31,13 @@ VerifyEmailOTP:
PreHeader: Verify One-Time Password
Subject: Verify One-Time Password
Greeting: Hello {{.DisplayName}},
- Text: Please use the "Authenticate" button or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
+ Text: Please use the one-time password {{.OTP}} to authenticate at ZITADEL within the next five minutes or click the "Authenticate" button.
ButtonText: Authenticate
VerifySMSOTP:
- Text: Please visit {{ .VerifyURL }} or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
+ Text: >-
+ {{.OTP}} is your one-time-password for {{ .Domain }}. Use it within the next {{.Expiry}}.
+
+ @{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domain has been claimed
PreHeader: Change email / username
diff --git a/internal/notification/static/i18n/es.yaml b/internal/notification/static/i18n/es.yaml
index 9a1413219..b2534e240 100644
--- a/internal/notification/static/i18n/es.yaml
+++ b/internal/notification/static/i18n/es.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Por favor, utiliza el botón 'Autenticar' o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
ButtonText: Autenticar
VerifySMSOTP:
- Text: Por favor, visita {{ .VerifyURL }} o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
+ Text: >-
+ {{.OTP}} es su contraseña de un solo uso para {{ .Domain }}. Úselo dentro de los próximos {{.Expiry}}.
+
+ @{{.Dominio}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Se ha reclamado un dominio
PreHeader: Cambiar dirección de correo electrónico / nombre de usuario
diff --git a/internal/notification/static/i18n/fr.yaml b/internal/notification/static/i18n/fr.yaml
index fccaaa61f..43d03253a 100644
--- a/internal/notification/static/i18n/fr.yaml
+++ b/internal/notification/static/i18n/fr.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Utilise le bouton 'Authentifier' ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
ButtonText: Authentifier
VerifySMSOTP:
- Text: Visite {{ .VerifyURL }} ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
+ Text: >-
+ {{.OTP}} est votre mot de passe à usage unique pour {{ .Domain }}. Utilisez-le dans le prochain {{.Expiry}}.
+
+ @{{.Domaine}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Le domaine a été réclamé
PreHeader: Modifier l'email / le nom d'utilisateur
diff --git a/internal/notification/static/i18n/it.yaml b/internal/notification/static/i18n/it.yaml
index 352aec4af..ef6d4c5c8 100644
--- a/internal/notification/static/i18n/it.yaml
+++ b/internal/notification/static/i18n/it.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Per favore, utilizza il pulsante 'Autentica' o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
ButtonText: Autentica
VerifySMSOTP:
- Text: Per favore, visita {{ .VerifyURL }} o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
+ Text: >-
+ {{.OTP}} è la tua password monouso per {{ .Domain }}. Usalo entro il prossimo {{.Expiry}}.
+
+ @{{.Dominio}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Il dominio è stato rivendicato
PreHeader: Cambiare email / nome utente
diff --git a/internal/notification/static/i18n/ja.yaml b/internal/notification/static/i18n/ja.yaml
index 53c1ac368..1ff5e1eb5 100644
--- a/internal/notification/static/i18n/ja.yaml
+++ b/internal/notification/static/i18n/ja.yaml
@@ -31,10 +31,13 @@ VerifyEmailOTP:
PreHeader: ワンタイムパスワードを確認する
Subject: ワンタイムパスワードを確認する
Greeting: こんにちは、{{.DisplayName}}さん
- Text: '認証'ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
+ Text: 認証ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
ButtonText: 認証
VerifySMSOTP:
- Text: {{ .VerifyURL }} を訪れるか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
+ Text: >-
+ {{.OTP}} は、{{ .Domain }} のワンタイムパスワードです。次の {{.Expiry}} 以内に使用してください。
+
+ @{{.ドメイン}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - ドメインの登録
PreHeader: メールアドレス・ユーザー名の変更
diff --git a/internal/notification/static/i18n/mk.yaml b/internal/notification/static/i18n/mk.yaml
index f68cc4c8b..0614efde7 100644
--- a/internal/notification/static/i18n/mk.yaml
+++ b/internal/notification/static/i18n/mk.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Ве молам, користи го копчето 'Автентицирај' или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
ButtonText: Автентицирај
VerifySMSOTP:
- Text: Ве молам, посети го {{ .VerifyURL }} или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
+ Text: >-
+ {{.OTP}} е вашата еднократна лозинка за {{ .Домен }}. Користете го во следниот {{.Истек}}.
+
+ @{{.Домен}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Доменот е преземен
PreHeader: Промена на е-пошта / корисничко име
diff --git a/internal/notification/static/i18n/pl.yaml b/internal/notification/static/i18n/pl.yaml
index 723e4d3b3..3f16dab3e 100644
--- a/internal/notification/static/i18n/pl.yaml
+++ b/internal/notification/static/i18n/pl.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Proszę, użyj przycisku 'Uwierzytelnij' lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
ButtonText: Uwierzytelnij
VerifySMSOTP:
- Text: Proszę, odwiedź {{ .VerifyURL }} lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
+ Text: >-
+ {{.OTP}} to Twoje jednorazowe hasło do domeny {{ .Domain }}. Użyj go w ciągu najbliższych {{.Expiry}}.
+
+ @{{.Domena}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domena została zarejestrowana
PreHeader: Zmiana adresu e-mail / nazwy użytkownika
diff --git a/internal/notification/static/i18n/pt.yaml b/internal/notification/static/i18n/pt.yaml
index 95f90fdd3..a7a2480b8 100644
--- a/internal/notification/static/i18n/pt.yaml
+++ b/internal/notification/static/i18n/pt.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Por favor, usa o botão 'Autenticar' ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
ButtonText: Autenticar
VerifySMSOTP:
- Text: Por favor, visita {{ .VerifyURL }} ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
+ Text: >-
+ {{.OTP}} é sua senha única para {{ .Domain }}. Use-o nos próximos {{.Expiry}}.
+
+ @{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domínio foi reivindicado
PreHeader: Alterar e-mail / nome de usuário
diff --git a/internal/notification/static/i18n/zh.yaml b/internal/notification/static/i18n/zh.yaml
index f36b2fdb2..b0dd21cad 100644
--- a/internal/notification/static/i18n/zh.yaml
+++ b/internal/notification/static/i18n/zh.yaml
@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: 请使用 '验证' 按钮,或复制一次性密码 {{.OTP}} 并将其粘贴到验证屏幕中,以在接下来的五分钟内在 ZITADEL 中进行验证。
ButtonText: 验证
VerifySMSOTP:
- Text: 请访问 {{ .VerifyURL }} 或复制一次性密码 {{.OTP}} 并将其粘贴到身份验证屏幕,以在接下来的五分钟内在ZITADEL进行身份验证。
+ Text: >-
+ {{.OTP}} 是您的 {{ .Domain }} 的一次性密码。在下一个 {{.Expiry}} 内使用它。
+
+ @{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - 域名所有权验证
PreHeader: 更改电子邮件/用户名
diff --git a/internal/notification/types/otp.go b/internal/notification/types/otp.go
new file mode 100644
index 000000000..aea3a5c12
--- /dev/null
+++ b/internal/notification/types/otp.go
@@ -0,0 +1,29 @@
+package types
+
+import (
+ "time"
+
+ "github.com/zitadel/zitadel/internal/api/ui/login"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+)
+
+func (notify Notify) SendOTPSMSCode(requestedDomain, origin, code string, expiry time.Duration) error {
+ args := otpArgs(code, origin, requestedDomain, expiry)
+ return notify("", args, domain.VerifySMSOTPMessageType, false)
+}
+
+func (notify Notify) SendOTPEmailCode(user *query.NotifyUser, requestedDomain, origin, code, authRequestID string, expiry time.Duration) error {
+ url := login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail)
+ args := otpArgs(code, origin, requestedDomain, expiry)
+ return notify(url, args, domain.VerifyEmailOTPMessageType, false)
+}
+
+func otpArgs(code, origin, requestedDomain string, expiry time.Duration) map[string]interface{} {
+ args := make(map[string]interface{})
+ args["OTP"] = code
+ args["Origin"] = origin
+ args["Domain"] = requestedDomain
+ args["Expiry"] = expiry
+ return args
+}
diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go
index 88eb744c4..f833aeb33 100644
--- a/internal/notification/types/phone_verification_code.go
+++ b/internal/notification/types/phone_verification_code.go
@@ -5,8 +5,9 @@ import (
"github.com/zitadel/zitadel/internal/query"
)
-func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error {
+func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code, requestedDomain string) error {
args := make(map[string]interface{})
args["Code"] = code
+ args["Domain"] = requestedDomain
return notify("", args, domain.VerifyPhoneMessageType, true)
}
diff --git a/internal/query/projection/user_auth_method.go b/internal/query/projection/user_auth_method.go
index ddaa53c2b..13b9c6db7 100644
--- a/internal/query/projection/user_auth_method.go
+++ b/internal/query/projection/user_auth_method.go
@@ -113,6 +113,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
Event: user.HumanOTPSMSRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
+ {
+ Event: user.HumanPhoneRemovedType,
+ Reduce: p.reduceRemoveAuthMethod,
+ },
+ {
+ Event: user.UserV1PhoneRemovedType,
+ Reduce: p.reduceRemoveAuthMethod,
+ },
{
Event: user.HumanOTPEmailRemovedType,
Reduce: p.reduceRemoveAuthMethod,
diff --git a/internal/repository/instance/secret_generator.go b/internal/repository/instance/secret_generator.go
index 013bd53d9..d2f25422d 100644
--- a/internal/repository/instance/secret_generator.go
+++ b/internal/repository/instance/secret_generator.go
@@ -168,7 +168,7 @@ func ChangeSecretGeneratorIncludeDigits(includeDigits bool) func(event *SecretGe
func ChangeSecretGeneratorIncludeSymbols(includeSymbols bool) func(event *SecretGeneratorChangedEvent) {
return func(e *SecretGeneratorChangedEvent) {
- e.IncludeDigits = &includeSymbols
+ e.IncludeSymbols = &includeSymbols
}
}
diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go
index 8aede0d19..414895d5a 100644
--- a/internal/repository/user/eventstore.go
+++ b/internal/repository/user/eventstore.go
@@ -91,10 +91,14 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckFailedType, HumanOTPCheckFailedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSAddedType, eventstore.GenericEventMapper[HumanOTPSMSAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSRemovedType, eventstore.GenericEventMapper[HumanOTPSMSRemovedEvent]).
+ RegisterFilterEventMapper(AggregateType, HumanOTPSMSCodeAddedType, eventstore.GenericEventMapper[HumanOTPSMSCodeAddedEvent]).
+ RegisterFilterEventMapper(AggregateType, HumanOTPSMSCodeSentType, eventstore.GenericEventMapper[HumanOTPSMSCodeSentEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckSucceededType, eventstore.GenericEventMapper[HumanOTPSMSCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckFailedType, eventstore.GenericEventMapper[HumanOTPSMSCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailAddedType, eventstore.GenericEventMapper[HumanOTPEmailAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailRemovedType, eventstore.GenericEventMapper[HumanOTPEmailRemovedEvent]).
+ RegisterFilterEventMapper(AggregateType, HumanOTPEmailCodeAddedType, eventstore.GenericEventMapper[HumanOTPEmailCodeAddedEvent]).
+ RegisterFilterEventMapper(AggregateType, HumanOTPEmailCodeSentType, eventstore.GenericEventMapper[HumanOTPEmailCodeSentEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckSucceededType, eventstore.GenericEventMapper[HumanOTPEmailCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckFailedType, eventstore.GenericEventMapper[HumanOTPEmailCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanU2FTokenAddedType, HumanU2FAddedEventMapper).
diff --git a/internal/repository/user/human_mfa_otp.go b/internal/repository/user/human_mfa_otp.go
index 3427dbbfc..52d5ad7a0 100644
--- a/internal/repository/user/human_mfa_otp.go
+++ b/internal/repository/user/human_mfa_otp.go
@@ -3,6 +3,7 @@ package user
import (
"context"
"encoding/json"
+ "time"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -21,11 +22,15 @@ const (
otpSMSEventPrefix = otpEventPrefix + "sms."
HumanOTPSMSAddedType = otpSMSEventPrefix + "added"
HumanOTPSMSRemovedType = otpSMSEventPrefix + "removed"
+ HumanOTPSMSCodeAddedType = otpSMSEventPrefix + "code.added"
+ HumanOTPSMSCodeSentType = otpSMSEventPrefix + "code.sent"
HumanOTPSMSCheckSucceededType = otpSMSEventPrefix + "check.succeeded"
HumanOTPSMSCheckFailedType = otpSMSEventPrefix + "check.failed"
otpEmailEventPrefix = otpEventPrefix + "email."
HumanOTPEmailAddedType = otpEmailEventPrefix + "added"
HumanOTPEmailRemovedType = otpEmailEventPrefix + "removed"
+ HumanOTPEmailCodeAddedType = otpEmailEventPrefix + "code.added"
+ HumanOTPEmailCodeSentType = otpEmailEventPrefix + "code.sent"
HumanOTPEmailCheckSucceededType = otpEmailEventPrefix + "check.succeeded"
HumanOTPEmailCheckFailedType = otpEmailEventPrefix + "check.failed"
)
@@ -271,6 +276,78 @@ func NewHumanOTPSMSRemovedEvent(
}
}
+type HumanOTPSMSCodeAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ Code *crypto.CryptoValue `json:"code,omitempty"`
+ Expiry time.Duration `json:"expiry,omitempty"`
+ *AuthRequestInfo
+}
+
+func (e *HumanOTPSMSCodeAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *HumanOTPSMSCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *HumanOTPSMSCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewHumanOTPSMSCodeAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ code *crypto.CryptoValue,
+ expiry time.Duration,
+ info *AuthRequestInfo,
+) *HumanOTPSMSCodeAddedEvent {
+ return &HumanOTPSMSCodeAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ HumanOTPSMSCodeAddedType,
+ ),
+ Code: code,
+ Expiry: expiry,
+ AuthRequestInfo: info,
+ }
+}
+
+type HumanOTPSMSCodeSentEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ Code *crypto.CryptoValue `json:"code,omitempty"`
+ Expiry time.Duration `json:"expiry,omitempty"`
+ *AuthRequestInfo
+}
+
+func (e *HumanOTPSMSCodeSentEvent) Data() interface{} {
+ return e
+}
+
+func (e *HumanOTPSMSCodeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *HumanOTPSMSCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewHumanOTPSMSCodeSentEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *HumanOTPSMSCodeSentEvent {
+ return &HumanOTPSMSCodeSentEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ HumanOTPSMSCodeSentType,
+ ),
+ }
+}
+
type HumanOTPSMSCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
@@ -393,6 +470,78 @@ func NewHumanOTPEmailRemovedEvent(
}
}
+type HumanOTPEmailCodeAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ Code *crypto.CryptoValue `json:"code,omitempty"`
+ Expiry time.Duration `json:"expiry,omitempty"`
+ *AuthRequestInfo
+}
+
+func (e *HumanOTPEmailCodeAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *HumanOTPEmailCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *HumanOTPEmailCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewHumanOTPEmailCodeAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ code *crypto.CryptoValue,
+ expiry time.Duration,
+ info *AuthRequestInfo,
+) *HumanOTPEmailCodeAddedEvent {
+ return &HumanOTPEmailCodeAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ HumanOTPEmailCodeAddedType,
+ ),
+ Code: code,
+ Expiry: expiry,
+ AuthRequestInfo: info,
+ }
+}
+
+type HumanOTPEmailCodeSentEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ Code *crypto.CryptoValue `json:"code,omitempty"`
+ Expiry time.Duration `json:"expiry,omitempty"`
+ *AuthRequestInfo
+}
+
+func (e *HumanOTPEmailCodeSentEvent) Data() interface{} {
+ return e
+}
+
+func (e *HumanOTPEmailCodeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *HumanOTPEmailCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewHumanOTPEmailCodeSentEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *HumanOTPEmailCodeSentEvent {
+ return &HumanOTPEmailCodeSentEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ HumanOTPEmailCodeSentType,
+ ),
+ }
+}
+
type HumanOTPEmailCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml
index b719bf191..319d4af86 100644
--- a/internal/static/i18n/bg.yaml
+++ b/internal/static/i18n/bg.yaml
@@ -630,6 +630,24 @@ EventTypes:
check:
succeeded: Многофакторната OTP проверка е успешна
failed: Многофакторната OTP проверка е неуспешна
+ sms:
+ added: Добавен Многофакторната OTP SMS
+ removed: Премахнато многофакторно OTP SMS
+ code:
+ added: Добавен многофакторен OTP SMS код
+ sent: Многофакторната OTP SMS код е изпратен
+ check:
+ succeeded: Успешна многофакторна OTP SMS проверка
+ failed: Многофакторната OTP проверка на SMS не бе успешна
+ email:
+ added: Добавен Многофакторната OTP имейл
+ removed: Премахнат многофакторен OTP имейл
+ code:
+ added: Добавен многофакторен OTP имейл код
+ sent: Многофакторният OTP имейл код е изпратен
+ check:
+ succeeded: Многофакторната еднократна имейл потвърждение е успешна
+ failed: Многофакторната OTP проверка на имейл не бе успешна
u2f:
token:
added: Добавен е многофакторен U2F токен
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index ff6c85d6a..774f87f7d 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Multifaktor OTP Verifikation erfolgreich
failed: Multifaktor OTP Verifikation fehlgeschlagen
+ sms:
+ added: Multifaktor OTP SMS hinzugefügt
+ removed: Multifaktor OTP SMS entfernt
+ code:
+ added: Multifaktor OTP SMS Code hinzugefügt
+ sent: Multifaktor OTP SMS Code versendet
+ check:
+ succeeded: Multifaktor OTP SMS Verifikation erfolgreich
+ failed: Multifaktor OTP SMS Verifikation fehlgeschlagen
+ email:
+ added: Multifaktor OTP Email hinzugefügt
+ removed: Multifaktor OTP Email entfernt
+ code:
+ added: Multifaktor OTP Email Code hinzugefügt
+ sent: Multifaktor OTP Email Code versendet
+ check:
+ succeeded: Multifaktor OTP Email Verifikation erfolgreich
+ failed: Multifaktor OTP Email Verifikation fehlgeschlagen
u2f:
token:
added: Multifaktor U2F Token hinzugefügt
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index 9058c15d9..d6e84e581 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Multifactor OTP check succeeded
failed: Multifactor OTP check failed
+ sms:
+ added: Multifactor OTP SMS added
+ removed: Multifactor OTP SMS removed
+ code:
+ added: Multifactor OTP SMS code added
+ sent: Multifactor OTP SMS code sent
+ check:
+ succeeded: Multifactor OTP SMS check succeeded
+ failed: Multifactor OTP SMS check failed
+ email:
+ added: Multifactor OTP Email added
+ removed: Multifactor OTP Email removed
+ code:
+ added: Multifactor OTP Email code added
+ sent: Multifactor OTP Email code sent
+ check:
+ succeeded: Multifactor OTP Email check succeeded
+ failed: Multifactor OTP Email check failed
u2f:
token:
added: Multifactor U2F Token added
diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml
index 475280504..95af0e071 100644
--- a/internal/static/i18n/es.yaml
+++ b/internal/static/i18n/es.yaml
@@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Comprobación exitosa de Multifactor OTP
failed: Comprobación fallida de Multifactor OTP
+ sms:
+ added: Multifactor OTP SMS añadido
+ removed: Multifactor OTP SMS elimonado
+ code:
+ added: Código Multifactor OTP SMS añadido
+ sent: Código Multifactor OTP SMS enviado
+ check:
+ succeeded: Comprobación Multifactor OTP SMS exitosa
+ failed: Comprobación Multifactor OTP SMS fallida
+ email:
+ added: Multifactor OTP email añadido
+ removed: Multifactor OTP email elimonado
+ code:
+ added: Código Multifactor OTP email añadido
+ sent: Código Multifactor OTP email enviado
+ check:
+ succeeded: Comprobación Multifactor OTP email exitosa
+ failed: Comprobación Multifactor OTP email fallida
u2f:
token:
added: Multifactor U2F Token añadido
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index 54ad40236..f0a1b1df4 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -616,6 +616,24 @@ EventTypes:
check:
succeeded: Vérification de l'OTP multifactorielle réussie
failed: La vérification de l'OTP multifactorielle a échoué
+ sms:
+ added: Ajout de SMS OTP multifactoriels
+ removed: Suppression des SMS OTP multifactoriels
+ code:
+ added: Ajout du code SMS OTP multifactoriel
+ sent: Code SMS OTP multifacteur envoyé
+ check:
+ succeeded: Vérification par SMS OTP multifacteur réussie
+ failed: Échec de la vérification par SMS OTP multifacteur
+ email:
+ added: Ajout d'un e-mail OTP multifactoriel
+ removed: Suppression de l'e-mail OTP multifacteur
+ code:
+ added: Ajout d'un code de messagerie OTP multifactoriel
+ sent: Code de messagerie OTP multifacteur envoyé
+ check:
+ succeeded: Vérification de l'e-mail OTP multifacteur réussie
+ failed: Échec de la vérification de l'e-mail OTP multifacteur
u2f:
token:
added: Ajout d'un jeton U2F multifacteur
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index 958eea5b1..3e70d4f6a 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -616,6 +616,24 @@ EventTypes:
check:
succeeded: Controllo OTP riuscito
failed: Controllo OTP fallito
+ sms:
+ added: Aggiunto SMS OTP
+ removed: OTP SMS rimosso
+ code:
+ added: Aggiunto codice OTP SMS
+ sent: Codice OTP SMS inviato
+ check:
+ succeeded: Controllo OTP SMS riuscito
+ failed: Controllo OTP SMS fallito
+ email:
+ added: Aggiunto OTP e-mail
+ removed: OTP e-mail rimosso
+ code:
+ added: Aggiunto codice OTP e-mail
+ sent: Codice OTP e-mail inviato
+ check:
+ succeeded: OTP Controllo e-mail riuscito
+ failed: OTP Controllo e-mail fallito
u2f:
token:
added: Aggiunto il U2F Token
diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml
index c0a735c1e..d1e5cdfa4 100644
--- a/internal/static/i18n/ja.yaml
+++ b/internal/static/i18n/ja.yaml
@@ -603,6 +603,24 @@ EventTypes:
check:
succeeded: MFA OTPチェックの成功
failed: MFA OTPチェックの失敗
+ sms:
+ added: 多要素 OTP SMS を追加しました
+ removed: 多要素 OTP SMS を削除しました
+ code:
+ added: 多要素 OTP SMS コードを追加しました
+ sent: 多要素 OTP SMS コードが送信されました
+ check:
+ succeeded: 多要素 OTP SMS 検証が成功しました
+ failed: 多要素 OTP SMS 検証が失敗しました
+ email:
+ added: 多要素 OTP 電子メールを追加しました
+ removed: 多要素 OTP 電子メールを削除しました
+ code:
+ added: 多要素 OTP 電子メール コードを追加しました
+ sent: 多要素 OTP 電子メール コードが送信されました
+ check:
+ succeeded: 多要素 OTP 電子メール検証が成功しました
+ failed: 多要素 OTP 電子メール検証が失敗しました
u2f:
token:
added: MFA U2Fトークンの追加
diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml
index 36d1b1c74..4d2afce3c 100644
--- a/internal/static/i18n/mk.yaml
+++ b/internal/static/i18n/mk.yaml
@@ -614,6 +614,24 @@ EventTypes:
check:
succeeded: Проверката на мултифактор OTP е успешна
failed: Проверката на мултифактор OTP е неуспешна
+ sms:
+ added: Додадена е мултифакторна OTP SMS
+ removed: Отстранета мултифакторна OTP SMS
+ code:
+ added: Додаден мултифакторски OTP SMS код
+ sent: Мултифакторен OTP СМС-код е испратен
+ check:
+ succeeded: Мултифакторна OTP СМС-верификација е успешна
+ failed: Мултифакторна OTP СМС-верификација не успеа
+ email:
+ added: Додадена е повеќефакторна OTP е-пошта
+ removed: Отстранета мултифакторна OTP е-пошта
+ code:
+ added: Додаден мултифакторски OTP код за е-пошта
+ sent: Испратен е-пошта OTP-код со повеќе фактори
+ check:
+ succeeded: Успешна е-пошта OTP-верификација на мултифактор
+ failed: Неуспешна потврда на е-пошта OTP со повеќе фактори
u2f:
token:
added: Додаден мултифактор U2F токен
diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml
index 1e831f682..adde4501a 100644
--- a/internal/static/i18n/pl.yaml
+++ b/internal/static/i18n/pl.yaml
@@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Sprawdzenie wielofaktorowego OTP zakończone powodzeniem
failed: Sprawdzenie wielofaktorowego OTP nie powiodło się
+ sms:
+ added: Dodano wieloskładnikowy SMS OTP
+ removed: Usunięto wieloskładnikowy SMS OTP
+ code:
+ added: Dodano wieloczynnikowy kod SMS OTP
+ sent: Wysłano kod SMS Multifactor OTP
+ check:
+ succeeded: Pomyślna weryfikacja wieloczynnikowego SMS-a OTP
+ failed: Wieloskładnikowa weryfikacja wiadomości SMS OTP nie powiodła się
+ email:
+ added: Dodano wieloskładnikowy e-mail OTP
+ removed: Usunięto wieloskładnikowy e-mail OTP
+ code:
+ added: Dodano wieloskładnikowy kod e-mail OTP
+ sent: Wysłano kod e-mail Multifactor OTP
+ check:
+ succeeded: Pomyślna wieloczynnikowa weryfikacja adresu e-mail OTP
+ failed: Wieloczynnikowa weryfikacja adresu e-mail OTP nie powiodła się
u2f:
token:
added: Dodano token wielofaktorowego U2F
diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml
index 9b823dbfe..00f3f1a05 100644
--- a/internal/static/i18n/pt.yaml
+++ b/internal/static/i18n/pt.yaml
@@ -609,6 +609,24 @@ EventTypes:
check:
succeeded: Verificação de OTP de autenticação multifator bem-sucedida
failed: Verificação de OTP de autenticação multifator falhou
+ sms:
+ added: Adicionado SMS OTP multifator
+ removed: SMS OTP multifator removido
+ code:
+ added: Adicionado código SMS OTP multifator
+ sent: Código SMS OTP multifator enviado
+ check:
+ succeeded: Verificação multifator OTP SMS bem-sucedida
+ failed: Falha na verificação multifator OTP SMS
+ email:
+ added: Adicionado e-mail OTP multifator
+ removed: E-mail OTP multifator removido
+ code:
+ added: Adicionado código de e-mail OTP multifator
+ sent: Código de e-mail OTP multifator enviado
+ check:
+ succeeded: Verificação de e-mail OTP multifator bem-sucedida
+ failed: Falha na verificação de e-mail OTP multifator
u2f:
token:
added: Token U2F de autenticação multifator adicionado
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index c1d7229cc..fb1d158c6 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -612,6 +612,24 @@ EventTypes:
check:
succeeded: 验证 MFA OTP 成功
failed: 验证 MFA OTP 失败
+ sms:
+ added: 添加了多因素 OTP 短信
+ removed: 删除了多因素 OTP 短信
+ code:
+ added: 添加了多因素 OTP 短信代码
+ sent: 已发送多因素 OTP 短信代码
+ check:
+ succeeded: 多因素 OTP 短信验证成功
+ failed: 多因素 OTP 短信验证失败
+ email:
+ added: 添加了多因素 OTP 电子邮件
+ removed: 删除了多因素 OTP 电子邮件
+ code:
+ added: 添加了多因素 OTP 电子邮件代码
+ sent: 已发送多因素 OTP 电子邮件代码
+ check:
+ succeeded: 多因素 OTP 电子邮件验证成功
+ failed: 多因素 OTP 电子邮件验证失败
u2f:
token:
added: 添加 MFA U2F 令牌
diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go
index 5bc7c9a11..dac8b8a55 100644
--- a/internal/user/model/user_view.go
+++ b/internal/user/model/user_view.go
@@ -49,6 +49,8 @@ type HumanView struct {
Region string
StreetAddress string
OTPState MFAState
+ OTPSMSAdded bool
+ OTPEmailAdded bool
U2FTokens []*WebAuthNView
PasswordlessTokens []*WebAuthNView
MFAMaxSetUp domain.MFALevel
@@ -162,10 +164,17 @@ func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.L
}
case domain.SecondFactorTypeU2F:
types = append(types, domain.MFATypeU2F)
+ case domain.SecondFactorTypeOTPSMS:
+ if !u.OTPSMSAdded {
+ types = append(types, domain.MFATypeOTPSMS)
+ }
+ case domain.SecondFactorTypeOTPEmail:
+ if !u.OTPEmailAdded {
+ types = append(types, domain.MFATypeOTPEmail)
+ }
}
}
}
- //PLANNED: add sms
}
return types
}
@@ -189,10 +198,17 @@ func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPo
if u.IsU2FReady() {
types = append(types, domain.MFATypeU2F)
}
+ case domain.SecondFactorTypeOTPSMS:
+ if u.OTPSMSAdded {
+ types = append(types, domain.MFATypeOTPSMS)
+ }
+ case domain.SecondFactorTypeOTPEmail:
+ if u.OTPEmailAdded {
+ types = append(types, domain.MFATypeOTPEmail)
+ }
}
}
}
- //PLANNED: add sms
}
return types, required
}
diff --git a/internal/user/repository/view/model/user.go b/internal/user/repository/view/model/user.go
index d5d913478..db9f50371 100644
--- a/internal/user/repository/view/model/user.go
+++ b/internal/user/repository/view/model/user.go
@@ -89,6 +89,8 @@ type HumanView struct {
Region string `json:"region" gorm:"column:region"`
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
OTPState int32 `json:"-" gorm:"column:otp_state"`
+ OTPSMSAdded bool `json:"-" gorm:"column:otp_sms_added"`
+ OTPEmailAdded bool `json:"-" gorm:"column:otp_email_added"`
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
@@ -178,6 +180,8 @@ func UserToModel(user *UserView) *model.UserView {
Region: user.Region,
StreetAddress: user.StreetAddress,
OTPState: model.MFAState(user.OTPState),
+ OTPSMSAdded: user.OTPSMSAdded,
+ OTPEmailAdded: user.OTPEmailAdded,
MFAMaxSetUp: domain.MFALevel(user.MFAMaxSetUp),
MFAInitSkipped: user.MFAInitSkipped,
InitRequired: user.InitRequired,
@@ -301,6 +305,8 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
user.HumanPhoneRemovedType:
u.Phone = ""
u.IsPhoneVerified = false
+ u.OTPSMSAdded = false
+ u.MFAInitSkipped = time.Time{}
case user.UserDeactivatedType:
u.State = int32(model.UserStateInactive)
case user.UserReactivatedType,
@@ -326,6 +332,16 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
case user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType:
u.OTPState = int32(model.MFAStateUnspecified)
+ case user.HumanOTPSMSAddedType:
+ u.OTPSMSAdded = true
+ case user.HumanOTPSMSRemovedType:
+ u.OTPSMSAdded = false
+ u.MFAInitSkipped = time.Time{}
+ case user.HumanOTPEmailAddedType:
+ u.OTPEmailAdded = true
+ case user.HumanOTPEmailRemovedType:
+ u.OTPEmailAdded = false
+ u.MFAInitSkipped = time.Time{}
case user.HumanU2FTokenAddedType:
err = u.addU2FToken(event)
case user.HumanU2FTokenVerifiedType:
@@ -520,7 +536,8 @@ func (u *UserView) ComputeMFAMaxSetUp() {
return
}
}
- if u.OTPState == int32(model.MFAStateReady) {
+ if u.OTPState == int32(model.MFAStateReady) ||
+ u.OTPSMSAdded || u.OTPEmailAdded {
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
return
}
@@ -575,6 +592,10 @@ func (u *UserView) EventTypes() []models.EventType {
models.EventType(user.HumanMFAOTPVerifiedType),
models.EventType(user.UserV1MFAOTPRemovedType),
models.EventType(user.HumanMFAOTPRemovedType),
+ models.EventType(user.HumanOTPSMSAddedType),
+ models.EventType(user.HumanOTPSMSRemovedType),
+ models.EventType(user.HumanOTPEmailAddedType),
+ models.EventType(user.HumanOTPEmailRemovedType),
models.EventType(user.HumanU2FTokenAddedType),
models.EventType(user.HumanU2FTokenVerifiedType),
models.EventType(user.HumanU2FTokenRemovedType),
diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go
index ec611c16f..63eb5e0a7 100644
--- a/internal/user/repository/view/model/user_session.go
+++ b/internal/user/repository/view/model/user_session.go
@@ -139,12 +139,32 @@ func (v *UserSessionView) AppendEvent(event *models.Event) error {
case user.UserV1MFAOTPCheckSucceededType,
user.HumanMFAOTPCheckSucceededType:
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeTOTP)
+ case user.HumanOTPSMSCheckSucceededType:
+ data := new(es_model.OTPVerified)
+ err := data.SetData(event)
+ if err != nil {
+ return err
+ }
+ if v.UserAgentID == data.UserAgentID {
+ v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTPSMS)
+ }
+ case user.HumanOTPEmailCheckSucceededType:
+ data := new(es_model.OTPVerified)
+ err := data.SetData(event)
+ if err != nil {
+ return err
+ }
+ if v.UserAgentID == data.UserAgentID {
+ v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTPEmail)
+ }
case user.UserV1MFAOTPCheckFailedType,
user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPCheckFailedType,
user.HumanMFAOTPRemovedType,
user.HumanU2FTokenCheckFailedType,
- user.HumanU2FTokenRemovedType:
+ user.HumanU2FTokenRemovedType,
+ user.HumanOTPSMSCheckFailedType,
+ user.HumanOTPEmailCheckFailedType:
v.SecondFactorVerification = time.Time{}
case user.HumanU2FTokenVerifiedType:
data := new(es_model.WebAuthNVerify)
@@ -218,6 +238,10 @@ func (v *UserSessionView) EventTypes() []models.EventType {
models.EventType(user.UserV1MFAOTPRemovedType),
models.EventType(user.HumanMFAOTPCheckFailedType),
models.EventType(user.HumanMFAOTPRemovedType),
+ models.EventType(user.HumanOTPSMSCheckSucceededType),
+ models.EventType(user.HumanOTPSMSCheckFailedType),
+ models.EventType(user.HumanOTPEmailCheckSucceededType),
+ models.EventType(user.HumanOTPEmailCheckFailedType),
models.EventType(user.HumanU2FTokenCheckFailedType),
models.EventType(user.HumanU2FTokenRemovedType),
models.EventType(user.HumanU2FTokenVerifiedType),