mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:17:32 +00:00
feat(login): add OTP (email and sms) (#6353)
* feat: login with otp * fix(i18n): japanese translation * add missing files * fix provider change * add event types translations to en * add tests * resourceOwner * remove unused handler * fix: secret generators and add comments * add setup step * rename * linting * fix setup * improve otp handling * fix autocomplete * translations for login and notifications * translations for event types * changes from review * check selected mfa type
This commit is contained in:
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user