mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat: allow to force MFA local only (#6234)
This PR adds an option to the LoginPolicy to "Force MFA for local users", so that users authenticated through an IDP must not configure (and verify) an MFA.
This commit is contained in:
@@ -842,6 +842,7 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
|
||||
AllowRegister: policy.AllowRegister,
|
||||
AllowExternalIDP: policy.AllowExternalIDPs,
|
||||
ForceMFA: policy.ForceMFA,
|
||||
ForceMFALocalOnly: policy.ForceMFALocalOnly,
|
||||
SecondFactors: policy.SecondFactors,
|
||||
MultiFactors: policy.MultiFactors,
|
||||
PasswordlessType: policy.PasswordlessType,
|
||||
@@ -975,7 +976,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
|
||||
}
|
||||
}
|
||||
|
||||
step, ok, err := repo.mfaChecked(userSession, request, user)
|
||||
step, ok, err := repo.mfaChecked(userSession, request, user, isInternalLogin && len(request.LinkingUsers) == 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1094,9 +1095,9 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
|
||||
return &domain.PasswordStep{}
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView) (domain.NextStep, bool, error) {
|
||||
func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView, isInternalAuthentication bool) (domain.NextStep, bool, error) {
|
||||
mfaLevel := request.MFALevel()
|
||||
allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy)
|
||||
allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy, isInternalAuthentication)
|
||||
promptRequired := (user.MFAMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required)
|
||||
if promptRequired || !repo.mfaSkippedOrSetUp(user, request) {
|
||||
types := user.MFATypesSetupPossible(mfaLevel, request.LoginPolicy)
|
||||
|
@@ -1439,6 +1439,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
userSession *user_model.UserSessionView
|
||||
request *domain.AuthRequest
|
||||
user *user_model.UserView
|
||||
isInternal bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1472,6 +1473,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
isInternal: true,
|
||||
},
|
||||
nil,
|
||||
false,
|
||||
@@ -1490,6 +1492,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
isInternal: true,
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
@@ -1509,6 +1512,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
isInternal: true,
|
||||
},
|
||||
&domain.MFAPromptStep{
|
||||
MFAProviders: []domain.MFAType{
|
||||
@@ -1533,6 +1537,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
isInternal: true,
|
||||
},
|
||||
&domain.MFAPromptStep{
|
||||
Required: true,
|
||||
@@ -1557,6 +1562,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
MFAInitSkipped: testNow,
|
||||
},
|
||||
},
|
||||
isInternal: true,
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
@@ -1578,6 +1584,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
|
||||
isInternal: true,
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
@@ -1599,6 +1606,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{},
|
||||
isInternal: true,
|
||||
},
|
||||
|
||||
&domain.MFAVerificationStep{
|
||||
@@ -1607,11 +1615,107 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"external not checked or forced but set up, want step",
|
||||
args{
|
||||
request: &domain.AuthRequest{
|
||||
LoginPolicy: &domain.LoginPolicy{
|
||||
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
|
||||
SecondFactorCheckLifetime: 18 * time.Hour,
|
||||
},
|
||||
},
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: domain.MFALevelSecondFactor,
|
||||
OTPState: user_model.MFAStateReady,
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{},
|
||||
isInternal: false,
|
||||
},
|
||||
&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"external not forced but checked",
|
||||
args{
|
||||
request: &domain.AuthRequest{
|
||||
LoginPolicy: &domain.LoginPolicy{
|
||||
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
|
||||
SecondFactorCheckLifetime: 18 * time.Hour,
|
||||
},
|
||||
},
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: domain.MFALevelSecondFactor,
|
||||
OTPState: user_model.MFAStateReady,
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
|
||||
isInternal: false,
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"external not checked but required, want step",
|
||||
args{
|
||||
request: &domain.AuthRequest{
|
||||
LoginPolicy: &domain.LoginPolicy{
|
||||
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
|
||||
SecondFactorCheckLifetime: 18 * time.Hour,
|
||||
ForceMFA: true,
|
||||
},
|
||||
},
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{},
|
||||
isInternal: false,
|
||||
},
|
||||
&domain.MFAPromptStep{
|
||||
Required: true,
|
||||
MFAProviders: []domain.MFAType{
|
||||
domain.MFATypeOTP,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"external not checked but local required",
|
||||
args{
|
||||
request: &domain.AuthRequest{
|
||||
LoginPolicy: &domain.LoginPolicy{
|
||||
SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
|
||||
SecondFactorCheckLifetime: 18 * time.Hour,
|
||||
ForceMFA: true,
|
||||
ForceMFALocalOnly: true,
|
||||
},
|
||||
},
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{},
|
||||
isInternal: false,
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := &AuthRequestRepo{}
|
||||
got, ok, err := repo.mfaChecked(tt.args.userSession, tt.args.request, tt.args.user)
|
||||
got, ok, err := repo.mfaChecked(tt.args.userSession, tt.args.request, tt.args.user, tt.args.isInternal)
|
||||
if (tt.errFunc != nil && !tt.errFunc(err)) || (err != nil && tt.errFunc == nil) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
return
|
||||
|
Reference in New Issue
Block a user