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:
Livio Spring
2023-07-20 06:06:16 +02:00
committed by GitHub
parent 1c3a15ff57
commit fed15574f6
49 changed files with 488 additions and 94 deletions

View File

@@ -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)

View File

@@ -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