mirror of
https://github.com/zitadel/zitadel.git
synced 2026-08-17 16:35:14 -05:00
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:
co-authored by
Marco A.
parent
9bab764796
commit
7c3d26b23b
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user