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. <[email protected]>
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)
})
}
}