feat: rt sessions OTP email challenge (#11941)

# Which Problems Are Solved

Add OTP Email challenge needed for Create/Set session in the RT model.

# How the Problems Are Solved

Introduce `OTPEmailChallengeCommand` (validate/execute/events) for
session creation checks
Add unit tests

# Additional Changes
Remove unused fields from `session_challenge_otp_sms.go`

# Additional Context
- Related to https://github.com/zitadel/zitadel/issues/11035

---------

Co-authored-by: Marco A. <kwbmm1990@gmail.com>
This commit is contained in:
Gayathri Vijayan
2026-03-31 11:50:53 +00:00
committed by GitHub
co-authored by Marco A.
parent 9bab764796
commit 7c3d26b23b
11 changed files with 1607 additions and 102 deletions
@@ -0,0 +1,36 @@
package convert
import (
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/internal/zerrors"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func ChallengeOTPEmailGRPCToDomain(otpEmailChallenge *session_grpc.RequestChallenges_OTPEmail) (*domain.ChallengeTypeOTPEmail, error) {
if otpEmailChallenge == nil {
return nil, nil
}
switch t := otpEmailChallenge.DeliveryType.(type) {
case *session_grpc.RequestChallenges_OTPEmail_SendCode_:
return &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{
SendCode: &domain.SendCode{
URLTemplate: t.SendCode.GetUrlTemplate(),
},
},
}, nil
case *session_grpc.RequestChallenges_OTPEmail_ReturnCode_:
return &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{
ReturnCode: true,
},
}, nil
case nil:
return &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{},
}, nil
default:
return nil, zerrors.ThrowUnimplementedf(nil, "SESSION-mfil3D", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t)
}
}
@@ -0,0 +1,82 @@
package convert
import (
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/backend/v3/domain"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func TestChallengeOTPEmailGRPCToDomain(t *testing.T) {
tests := []struct {
name string
otpEmailChallenge *session_grpc.RequestChallenges_OTPEmail
want *domain.ChallengeTypeOTPEmail
wantErr error
}{
{
name: "nil OTP email challenge",
},
{
name: "otp email challenge - delivery type send code without url template",
otpEmailChallenge: &session_grpc.RequestChallenges_OTPEmail{
DeliveryType: &session_grpc.RequestChallenges_OTPEmail_SendCode_{
SendCode: &session_grpc.RequestChallenges_OTPEmail_SendCode{},
},
},
want: &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{
SendCode: &domain.SendCode{},
},
},
},
{
name: "otp email challenge - delivery type send code with url template",
otpEmailChallenge: &session_grpc.RequestChallenges_OTPEmail{
DeliveryType: &session_grpc.RequestChallenges_OTPEmail_SendCode_{
SendCode: &session_grpc.RequestChallenges_OTPEmail_SendCode{
UrlTemplate: gu.Ptr("https://example.com/otp"),
},
},
},
want: &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{
SendCode: &domain.SendCode{
URLTemplate: "https://example.com/otp",
},
},
},
},
{
name: "otp email challenge - delivery type return code",
otpEmailChallenge: &session_grpc.RequestChallenges_OTPEmail{
DeliveryType: &session_grpc.RequestChallenges_OTPEmail_ReturnCode_{
ReturnCode: &session_grpc.RequestChallenges_OTPEmail_ReturnCode{},
},
},
want: &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{
ReturnCode: true,
},
},
},
{
name: "otp email challenge - delivery type not set",
otpEmailChallenge: &session_grpc.RequestChallenges_OTPEmail{},
want: &domain.ChallengeTypeOTPEmail{
DeliveryType: domain.DeliveryType{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ChallengeOTPEmailGRPCToDomain(tt.otpEmailChallenge)
assert.Equal(t, tt.want, got)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
@@ -0,0 +1,15 @@
package convert
import (
"github.com/zitadel/zitadel/backend/v3/domain"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func ChallengeOTPSMSGRPCToDomain(otpSMSChallenge *session_grpc.RequestChallenges_OTPSMS) *domain.ChallengeTypeOTPSMS {
if otpSMSChallenge == nil {
return nil
}
return &domain.ChallengeTypeOTPSMS{
ReturnCode: otpSMSChallenge.GetReturnCode(),
}
}
@@ -0,0 +1,48 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/backend/v3/domain"
session_grpc "github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
func TestChallengeOTPSMSGRPCToDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
challenge *session_grpc.RequestChallenges_OTPSMS
want *domain.ChallengeTypeOTPSMS
}{
{
name: "nil OTP SMS challenge",
},
{
name: "OTP SMS challenge true",
challenge: &session_grpc.RequestChallenges_OTPSMS{
ReturnCode: true,
},
want: &domain.ChallengeTypeOTPSMS{
ReturnCode: true,
},
},
{
name: "OTP SMS challenge false",
challenge: &session_grpc.RequestChallenges_OTPSMS{
ReturnCode: false,
},
want: &domain.ChallengeTypeOTPSMS{
ReturnCode: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ChallengeOTPSMSGRPCToDomain(tt.challenge)
assert.Equal(t, tt.want, got)
})
}
}
+14 -9
View File
@@ -9,15 +9,16 @@ import (
)
var (
pool database.Pool
legacyEventstore eventstore.LegacyEventstore
sysConfig systemdefaults.SystemDefaults
passwordHasher *crypto.Hasher
idpEncryptionAlgo crypto.EncryptionAlgorithm
sessionTokenDecryptor SessionTokenDecryptor
mfaEncryptionAlgo crypto.EncryptionAlgorithm
otpSMSSecretGeneratorConfig *crypto.GeneratorConfig
webauthnConfig *webauthn.Config
pool database.Pool
legacyEventstore eventstore.LegacyEventstore
sysConfig systemdefaults.SystemDefaults
passwordHasher *crypto.Hasher
idpEncryptionAlgo crypto.EncryptionAlgorithm
sessionTokenDecryptor SessionTokenDecryptor
mfaEncryptionAlgo crypto.EncryptionAlgorithm
otpSMSSecretGeneratorConfig *crypto.GeneratorConfig
otpEmailSecretGeneratorConfig *crypto.GeneratorConfig
webauthnConfig *webauthn.Config
)
func SetPool(p database.Pool) {
@@ -48,6 +49,10 @@ func SetOTPSMSSecretGeneratorConfig(cfg *crypto.GeneratorConfig) {
otpSMSSecretGeneratorConfig = cfg
}
func SetOTPEmailSecretGeneratorConfig(cfg *crypto.GeneratorConfig) {
otpEmailSecretGeneratorConfig = cfg
}
func SetMFAEncryptionAlgorithm(mfaEncryptionAlg crypto.EncryptionAlgorithm) {
mfaEncryptionAlgo = mfaEncryptionAlg
}
@@ -0,0 +1,276 @@
package domain
import (
"context"
"io"
"text/template"
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/zerrors"
)
var _ Commander = (*OTPEmailChallengeCommand)(nil)
var _ Transactional = (*OTPEmailChallengeCommand)(nil)
type SendCode struct {
URLTemplate string
}
type DeliveryType struct {
SendCode *SendCode
ReturnCode bool
}
type ChallengeTypeOTPEmail struct {
DeliveryType DeliveryType
}
type OTPEmailChallengeCommand struct {
ChallengeTypeOTPEmail *ChallengeTypeOTPEmail
SessionID string
InstanceID string
defaultSecretGeneratorConfig *crypto.GeneratorConfig
otpEncryptionAlgorithm crypto.EncryptionAlgorithm
newEmailCode newOTPCodeFunc
sessionChallengeOTPEmail *SessionChallengeOTPEmail // the generated OTP Email challenge that is stored in the session.
otpEmailChallenge *string // challenge to be set in the CreateSessionResponse
}
func NewOTPEmailChallengeCommand(
challengeTypeOTPEmail *ChallengeTypeOTPEmail,
sessionID string,
instanceID string,
secretGeneratorConfig *crypto.GeneratorConfig,
otpAlgorithm crypto.EncryptionAlgorithm,
newEmailCodeFn newOTPCodeFunc) *OTPEmailChallengeCommand {
if secretGeneratorConfig == nil {
secretGeneratorConfig = otpEmailSecretGeneratorConfig
}
if otpAlgorithm == nil {
otpAlgorithm = mfaEncryptionAlgo
}
if newEmailCodeFn == nil {
newEmailCodeFn = crypto.NewCode
}
return &OTPEmailChallengeCommand{
ChallengeTypeOTPEmail: challengeTypeOTPEmail,
SessionID: sessionID,
InstanceID: instanceID,
defaultSecretGeneratorConfig: secretGeneratorConfig,
otpEncryptionAlgorithm: otpAlgorithm,
newEmailCode: newEmailCodeFn,
}
}
// Validate implements [Commander].
// It validates that the session and user exist and that the user has email OTP enabled.
func (o *OTPEmailChallengeCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
if o.ChallengeTypeOTPEmail == nil {
return nil
}
err = o.validatePreConditions()
if err != nil {
return err
}
// get session
sessionRepo := opts.sessionRepo
retrievedSession, err := sessionRepo.Get(
ctx,
opts.DB(),
database.WithCondition(sessionRepo.PrimaryKeyCondition(o.InstanceID, o.SessionID)),
)
if err := handleGetError(err, "DOM-JArUai", objectTypeSession); err != nil {
return err
}
if retrievedSession.UserID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-wG2XoJ", "Errors.Missing.Session.UserID")
}
// get user
userRepo := opts.userRepo
retrievedUser, err := userRepo.Get(
ctx,
opts.DB(),
database.WithCondition(userRepo.PrimaryKeyCondition(o.InstanceID, retrievedSession.UserID)),
)
if err := handleGetError(err, "DOM-56MWkg", objectTypeUser); err != nil {
return err
}
// validate human user and user email
if retrievedUser.Human == nil || retrievedUser.Human.Email.Address == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-7hG2d", "Errors.NotFound.User.Human.Email")
}
// validate email OTP is enabled
if retrievedUser.Human.Email.OTP.EnabledAt.IsZero() {
return zerrors.ThrowPreconditionFailed(nil, "DOM-9kL4q", "Errors.User.MFA.OTP.NotReady")
}
return nil
}
// Execute implements [Commander].
// It updates the session with the generated OTP email challenge.
func (o *OTPEmailChallengeCommand) Execute(ctx context.Context, opts *InvokeOpts) error {
if o.ChallengeTypeOTPEmail == nil {
return nil
}
// prepare the otp email challenge
sessionChallengeOTPEmail, challenge, err := o.prepareOTPEmailChallenge(ctx, opts)
if err != nil {
return err
}
// update the session with the otp email challenge
sessionRepo := opts.sessionRepo
updated, err := sessionRepo.Update(
ctx,
opts.DB(),
sessionRepo.PrimaryKeyCondition(o.InstanceID, o.SessionID),
sessionRepo.SetChallenge(sessionChallengeOTPEmail),
)
if err := handleUpdateError(err, expectedUpdatedRows, updated, "DOM-YfQIA3", objectTypeSession); err != nil {
return err
}
o.sessionChallengeOTPEmail = sessionChallengeOTPEmail
if o.ChallengeTypeOTPEmail.DeliveryType.ReturnCode { // only set when the delivery type is ReturnCode
o.otpEmailChallenge = &challenge
}
return nil
}
// Events implements [Commander].
// It creates the OTPEmailChallengedEvent if an OTP email challenge was requested.
func (o *OTPEmailChallengeCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
if o.ChallengeTypeOTPEmail == nil {
return nil, nil
}
return []eventstore.Command{
session.NewOTPEmailChallengedEvent(
ctx,
&session.NewAggregate(o.SessionID, o.InstanceID).Aggregate,
o.sessionChallengeOTPEmail.Code,
o.sessionChallengeOTPEmail.Expiry,
o.sessionChallengeOTPEmail.CodeReturned,
o.sessionChallengeOTPEmail.URLTemplate,
),
}, nil
}
func (o *OTPEmailChallengeCommand) GetOTPEmailChallenge() *string {
return o.otpEmailChallenge
}
// prepareOTPEmailChallenge generates the OTP email challenge based on the delivery type in the request.
func (o *OTPEmailChallengeCommand) prepareOTPEmailChallenge(ctx context.Context, opts *InvokeOpts) (*SessionChallengeOTPEmail, string, error) {
// generate email code
config, err := GetOTPCryptoGeneratorConfigWithDefault(ctx, o.InstanceID, opts, o.defaultSecretGeneratorConfig, OTPTypeEmail)
if err != nil {
return nil, "", err
}
codeGenerator := crypto.NewEncryptionGenerator(*config, o.otpEncryptionAlgorithm)
crypted, plain, err := o.newEmailCode(codeGenerator)
if err != nil {
return nil, "", err
}
challengeOTPEmail := &SessionChallengeOTPEmail{
LastChallengedAt: time.Now(),
Code: crypted,
Expiry: config.Expiry,
TriggeredAtOrigin: http.DomainContext(ctx).Origin(),
}
var otpEmailChallenge string
switch {
case o.ChallengeTypeOTPEmail.DeliveryType.SendCode != nil:
challengeOTPEmail.URLTemplate = o.ChallengeTypeOTPEmail.DeliveryType.SendCode.URLTemplate
case o.ChallengeTypeOTPEmail.DeliveryType.ReturnCode:
challengeOTPEmail.CodeReturned = true
otpEmailChallenge = plain
default:
// no additional action needed
}
return challengeOTPEmail, otpEmailChallenge, nil
}
// String implements [Commander].
func (o *OTPEmailChallengeCommand) String() string {
return "OTPEmailChallengeCommand"
}
// RequiresTransaction implements [Transactional].
func (o *OTPEmailChallengeCommand) RequiresTransaction() {}
// validateURLTemplate renders the given URL template with sample data to validate its correctness.
func validateURLTemplate(w io.Writer, tmpl string) error {
otpEmailURLData := &struct {
Code string
UserID string
LoginName string
DisplayName string
PreferredLanguage language.Tag
SessionID string
}{
Code: "code",
UserID: "userID",
LoginName: "loginName",
DisplayName: "displayName",
PreferredLanguage: language.English,
SessionID: "SessionID",
}
parsed, err := template.New("").Parse(tmpl)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "DOM-wkDwQM", "Errors.Invalid.URLTemplate")
}
if err = parsed.Execute(w, otpEmailURLData); err != nil {
return zerrors.ThrowInvalidArgument(err, "DOM-F5Yv8l", "Errors.Invalid.URLTemplate")
}
return nil
}
func (o *OTPEmailChallengeCommand) validatePreConditions() error {
// validate required fields
if o.SessionID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-BQ5UgK", "Errors.Missing.SessionID")
}
if o.InstanceID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-kDnkDn", "Errors.Missing.InstanceID")
}
// validate that default secret generator config is set
if o.defaultSecretGeneratorConfig == nil {
return zerrors.ThrowInternal(nil, "DOM-nnB9MS", "missing default secret generator config")
}
// validate that otp encryption algorithm is set
if o.otpEncryptionAlgorithm == nil {
return zerrors.ThrowInternal(nil, "DOM-kuG75Q", "missing MFA encryption algorithm")
}
// validate the URL template
if sc := o.ChallengeTypeOTPEmail.DeliveryType.SendCode; sc != nil {
if sc.URLTemplate != "" {
if err := validateURLTemplate(io.Discard, sc.URLTemplate); err != nil {
return err
}
}
}
return nil
}
File diff suppressed because it is too large Load Diff
+4 -23
View File
@@ -42,9 +42,6 @@ type OTPSMSChallengeCommand struct {
smsProvider getActiveSMSProviderFn
newPhoneCode newOTPCodeFunc
session *Session
user *User
challengeOTPSMS *SessionChallengeOTPSMS // the generated OTP SMS challenge that is stored in the session.
otpSMSChallenge *string // challenge to be set in the CreateSessionResponse
}
@@ -117,20 +114,14 @@ func (o *OTPSMSChallengeCommand) Validate(ctx context.Context, opts *InvokeOpts)
return err
}
if retrievedUser.ID == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-1bzvsh", "Errors.User.UserIDMissing")
}
// validate human user and user phone
if retrievedUser.Human == nil || retrievedUser.Human.Phone == nil {
if retrievedUser.Human == nil || retrievedUser.Human.Phone == nil || retrievedUser.Human.Phone.Number == "" {
return zerrors.ThrowPreconditionFailed(nil, "DOM-7hG2w", "Errors.NotFound.User.Human.Phone")
}
// validate phone OTP is enabled
if retrievedUser.Human.Phone.OTP.EnabledAt.IsZero() {
return zerrors.ThrowPreconditionFailed(nil, "DOM-9kL4m", "Errors.OTPSMS.NotEnabled")
return zerrors.ThrowPreconditionFailed(nil, "DOM-9kL4m", "Errors.User.MFA.OTP.NotReady")
}
o.session = retrievedSession
o.user = retrievedUser
return nil
}
@@ -164,19 +155,9 @@ func (o *OTPSMSChallengeCommand) Execute(ctx context.Context, opts *InvokeOpts)
sessionRepo.PrimaryKeyCondition(o.InstanceID, o.SessionID),
sessionRepo.SetChallenge(challengeOTPSMS),
)
if err != nil {
return zerrors.ThrowInternal(err, "DOM-AigB0Z", "session update failed")
if err := handleUpdateError(err, expectedUpdatedRows, updateCount, "DOM-AigB0Z", objectTypeSession); err != nil {
return err
}
if updateCount == 0 {
return zerrors.ThrowNotFound(nil, "DOM-QThZH7", "Errors.Session.NotFound")
}
if updateCount > 1 {
return zerrors.ThrowInternal(NewMultipleObjectsUpdatedError(expectedUpdatedRows, updateCount), "DOM-gYp8tG", "unexpected number of rows")
}
// todo (@grvijayan): uncomment after these changes are available
// if err := handleUpdateError(err, expectedUpdatedRows, updated, "DOM-AigB0Z", objectTypeSession); err != nil {
// return err
// }
o.challengeOTPSMS = challengeOTPSMS
if o.ChallengeTypeOTPSMS.ReturnCode {
o.otpSMSChallenge = &plain
@@ -185,16 +185,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.PrimaryKeyCondition("instance-1", "session-1"),
),
)).
Times(1).
Return(&domain.Session{
UserID: "user-1",
}, nil)
getSessionSucceededExpectation(repo)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
@@ -226,16 +217,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.PrimaryKeyCondition("instance-1", "session-1"),
),
)).
Times(1).
Return(&domain.Session{
UserID: "user-1",
}, nil)
getSessionSucceededExpectation(repo)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
@@ -267,16 +249,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.PrimaryKeyCondition("instance-1", "session-1"),
),
)).
Times(1).
Return(&domain.Session{
UserID: "user-1",
}, nil)
getSessionSucceededExpectation(repo)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
@@ -314,16 +287,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.PrimaryKeyCondition("instance-1", "session-1"),
),
)).
Times(1).
Return(&domain.Session{
UserID: "user-1",
}, nil)
getSessionSucceededExpectation(repo)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
@@ -364,16 +328,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
database.WithCondition(
repo.PrimaryKeyCondition("instance-1", "session-1"),
),
)).
Times(1).
Return(&domain.Session{
UserID: "user-1",
}, nil)
getSessionSucceededExpectation(repo)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
@@ -401,7 +356,7 @@ func TestOTPSMSChallengeCommand_Validate(t *testing.T) {
}, nil)
return repo
},
wantErr: zerrors.ThrowPreconditionFailed(nil, "DOM-9kL4m", "Errors.OTPSMS.NotEnabled"),
wantErr: zerrors.ThrowPreconditionFailed(nil, "DOM-9kL4m", "Errors.User.MFA.OTP.NotReady"),
},
{
name: "valid OTP SMS challenge request",
@@ -819,7 +774,7 @@ func TestOTPSMSChallengeCommand_Execute(t *testing.T) {
updateSessionFailedExpectation(repo, challengeOTPSMSChange, assert.AnError, 0)
return repo
},
wantErr: zerrors.ThrowInternal(assert.AnError, "DOM-AigB0Z", "session update failed"),
wantErr: zerrors.ThrowInternal(assert.AnError, "DOM-AigB0Z", "failed updating Session"),
},
{
name: "failed to update session - no rows updated",
@@ -862,7 +817,7 @@ func TestOTPSMSChallengeCommand_Execute(t *testing.T) {
updateSessionFailedExpectation(repo, challengeOTPSMSChange, nil, 0)
return repo
},
wantErr: zerrors.ThrowNotFound(nil, "DOM-QThZH7", "Errors.Session.NotFound"),
wantErr: zerrors.ThrowNotFound(nil, "DOM-AigB0Z", "Session not found"),
},
{
name: "failed to update session - more than 1 row updated",
@@ -904,7 +859,7 @@ func TestOTPSMSChallengeCommand_Execute(t *testing.T) {
updateSessionFailedExpectation(repo, challengeOTPSMSChange, nil, 2)
return repo
},
wantErr: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-gYp8tG", "unexpected number of rows"),
wantErr: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-AigB0Z", "unexpected number of rows updated"),
},
}
for _, tt := range tests {
@@ -1263,6 +1218,12 @@ func getUser(otpEnabledAt time.Time) func(ctrl *gomock.Controller) domain.UserRe
EnabledAt: otpEnabledAt,
},
},
Email: domain.HumanEmail{
Address: "testuser@example.com",
OTP: domain.OTP{
EnabledAt: otpEnabledAt,
},
},
},
}, nil)
return repo
@@ -1320,17 +1320,11 @@ func updateHumanUserFailedExpectation(ctrl *gomock.Controller, userRepo *domainm
userUpdates = append(userUpdates, humanRepo.SetState(domain.UserStateLocked))
}
userRepo.EXPECT().Human().Times(1).Return(humanRepo)
if err != nil {
humanRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), primaryKeyCondition, userUpdates).
Times(1).
Return(int64(0), err)
return
}
humanRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), primaryKeyCondition, userUpdates).
Times(1).
Return(updateCount, nil)
Return(updateCount, err)
}
func getUserSucceededExpectation(userRepo *domainmock.UserRepo, failedAttempts uint8) {
@@ -1363,17 +1357,10 @@ func updateSessionSucceededExpectation(sessionRepo *domainmock.SessionRepo, chan
func updateSessionFailedExpectation(sessionRepo *domainmock.SessionRepo, change database.Change, err error, updateCount int64) {
primaryKeyCondition := sessionRepo.PrimaryKeyCondition("instance-1", "session-1")
if err != nil {
sessionRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), primaryKeyCondition, change).
Times(1).
Return(updateCount, err)
return
}
sessionRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), primaryKeyCondition, change).
Times(1).
Return(updateCount, nil)
Return(updateCount, err)
}
func getSessionSucceededExpectation(sessionRepo *domainmock.SessionRepo) {
+1
View File
@@ -212,6 +212,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
new_domain.SetIDPEncryptionAlgorithm(keys.IDPConfig)
new_domain.SetMFAEncryptionAlgorithm(keys.OTP)
new_domain.SetOTPSMSSecretGeneratorConfig(config.DefaultInstance.SecretGenerators.OTPSMS)
new_domain.SetOTPEmailSecretGeneratorConfig(config.DefaultInstance.SecretGenerators.OTPEmail)
sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)
sessionTokenDecryptor := internal_authz.SessionTokenDecryptor(keys.OIDC)