package command import ( "context" "io" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func (c *Commands) CreateOTPSMSChallengeReturnCode(dst *string) SessionCommand { return c.createOTPSMSChallenge(true, dst) } func (c *Commands) CreateOTPSMSChallenge() SessionCommand { return c.createOTPSMSChallenge(false, nil) } func (c *Commands) createOTPSMSChallenge(returnCode bool, dst *string) SessionCommand { return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) { if cmd.sessionWriteModel.UserID == "" { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKL3g", "Errors.User.UserIDMissing") } writeModel := NewHumanOTPSMSWriteModel(cmd.sessionWriteModel.UserID, "") if err := cmd.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { return nil, err } if !writeModel.OTPAdded() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady") } code, generatorID, err := cmd.createPhoneCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPSMS, cmd.otpAlg, c.defaultSecretGenerators.OTPSMS) //nolint:staticcheck if err != nil { return nil, err } if returnCode { *dst = code.Plain } cmd.OTPSMSChallenged(ctx, code.CryptedCode(), code.CodeExpiry(), returnCode, generatorID) return nil, nil } } func (c *Commands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { sessionWriteModel := NewSessionWriteModel(sessionID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return err } if sessionWriteModel.OTPSMSCodeChallenge == nil { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-G3t31", "Errors.User.Code.NotFound") } return c.pushAppendAndReduce(ctx, sessionWriteModel, session.NewOTPSMSSentEvent(ctx, &session.NewAggregate(sessionID, sessionWriteModel.ResourceOwner).Aggregate, generatorInfo), ) } func (c *Commands) CreateOTPEmailChallengeURLTemplate(urlTmpl string) (SessionCommand, error) { if err := domain.RenderOTPEmailURLTemplate(io.Discard, urlTmpl, "code", "userID", "loginName", "displayName", language.English); err != nil { return nil, err } return c.createOTPEmailChallenge(false, urlTmpl, nil), nil } func (c *Commands) CreateOTPEmailChallengeReturnCode(dst *string) SessionCommand { return c.createOTPEmailChallenge(true, "", dst) } func (c *Commands) CreateOTPEmailChallenge() SessionCommand { return c.createOTPEmailChallenge(false, "", nil) } func (c *Commands) createOTPEmailChallenge(returnCode bool, urlTmpl string, dst *string) SessionCommand { return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) { if cmd.sessionWriteModel.UserID == "" { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing") } writeModel := NewHumanOTPEmailWriteModel(cmd.sessionWriteModel.UserID, "") if err := cmd.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { return nil, err } if !writeModel.OTPAdded() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady") } code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPEmail, cmd.otpAlg, c.defaultSecretGenerators.OTPEmail) //nolint:staticcheck if err != nil { return nil, err } if returnCode { *dst = code.Plain } cmd.OTPEmailChallenged(ctx, code.Crypted, code.Expiry, returnCode, urlTmpl) return nil, nil } } func (c *Commands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error { sessionWriteModel := NewSessionWriteModel(sessionID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return err } if sessionWriteModel.OTPEmailCodeChallenge == nil { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SLr02", "Errors.User.Code.NotFound") } return c.pushAppendAndReduce(ctx, sessionWriteModel, session.NewOTPEmailSentEvent(ctx, &session.NewAggregate(sessionID, sessionWriteModel.ResourceOwner).Aggregate), ) } func CheckOTPSMS(code string) SessionCommand { return func(ctx context.Context, cmd *SessionCommands) (_ []eventstore.Command, err error) { writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) { otpWriteModel := NewHumanOTPSMSCodeWriteModel(cmd.sessionWriteModel.UserID, "") err := cmd.eventstore.FilterToQueryReducer(ctx, otpWriteModel) if err != nil { return nil, err } // explicitly set the challenge from the session write model since the code write model will only check user events otpWriteModel.otpCode = cmd.sessionWriteModel.OTPSMSCodeChallenge return otpWriteModel, nil } succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPSMSCheckSucceededEvent(ctx, aggregate, nil) } failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, nil) } commands, err := checkOTP( ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, cmd.getCodeVerifier, succeededEvent, failedEvent, ) if err != nil { return commands, err } cmd.eventCommands = append(cmd.eventCommands, commands...) cmd.OTPSMSChecked(ctx, cmd.now()) return nil, nil } } func CheckOTPEmail(code string) SessionCommand { return func(ctx context.Context, cmd *SessionCommands) (_ []eventstore.Command, err error) { writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) { otpWriteModel := NewHumanOTPEmailCodeWriteModel(cmd.sessionWriteModel.UserID, "") err := cmd.eventstore.FilterToQueryReducer(ctx, otpWriteModel) if err != nil { return nil, err } // explicitly set the challenge from the session write model since the code write model will only check user events otpWriteModel.otpCode = cmd.sessionWriteModel.OTPEmailCodeChallenge return otpWriteModel, nil } succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPEmailCheckSucceededEvent(ctx, aggregate, nil) } failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command { return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, nil) } commands, err := checkOTP( ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, nil, // email currently always uses local code checks succeededEvent, failedEvent, ) if err != nil { return commands, err } cmd.eventCommands = append(cmd.eventCommands, commands...) cmd.OTPEmailChecked(ctx, cmd.now()) return nil, nil } }