Files
zitadel/internal/api/ui/login/mfa_init_test.go
T
522b0ed7ed Merge commit from fork
The Login V1 MFA init/enrollment handlers acted on authReq.UserID via
setUserContext without verifying the auth request had completed its
first factor. A request that only submitted a loginname could enroll
TOTP/SMS/Email/U2F factors and overwrite the victim's phone number, and
the discrepant errors bypassed IgnoreUnknownUsernames.

Gate the enrollment handlers on PossibleSteps[0] == *domain.MFAPromptStep,
which nextSteps only yields after firstFactorChecked passes.

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-08-14 06:12:04 +02:00

76 lines
1.9 KiB
Go

package login
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
)
// Test_isMFAPromptStep guards the fix for the unauthenticated MFA-enrollment / phone-overwrite
// issue: MFA init/enrollment handlers must only proceed when the auth request has legitimately
// reached the MFA prompt step (i.e. after the first factor was checked). A request that only
// submitted a loginname carries a first-factor step, not an *domain.MFAPromptStep.
func Test_isMFAPromptStep(t *testing.T) {
t.Parallel()
tests := []struct {
name string
authReq *domain.AuthRequest
want bool
}{
{
name: "nil auth request",
authReq: nil,
want: false,
},
{
name: "no possible steps",
authReq: &domain.AuthRequest{},
want: false,
},
{
name: "loginname only - first factor (password) step",
authReq: &domain.AuthRequest{
PossibleSteps: []domain.NextStep{&domain.PasswordStep{}},
},
want: false,
},
{
name: "existing factor - MFA verification step, not enrollment",
authReq: &domain.AuthRequest{
PossibleSteps: []domain.NextStep{
&domain.MFAVerificationStep{MFAProviders: []domain.MFAType{domain.MFATypeTOTP}},
},
},
want: false,
},
{
name: "legitimate enrollment - MFA prompt step",
authReq: &domain.AuthRequest{
PossibleSteps: []domain.NextStep{
&domain.MFAPromptStep{Required: true, MFAProviders: []domain.MFAType{domain.MFATypeTOTP}},
},
},
want: true,
},
{
name: "MFA prompt step not first - only the current step counts",
authReq: &domain.AuthRequest{
PossibleSteps: []domain.NextStep{
&domain.PasswordStep{},
&domain.MFAPromptStep{Required: true, MFAProviders: []domain.MFAType{domain.MFATypeTOTP}},
},
},
want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, isMFAPromptStep(tc.authReq))
})
}
}