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

@@ -430,6 +430,7 @@ func (s *Server) getLoginPolicy(ctx context.Context, orgID string, orgIDPs []str
AllowRegister: queriedLogin.AllowRegister,
AllowExternalIdp: queriedLogin.AllowExternalIDPs,
ForceMfa: queriedLogin.ForceMFA,
ForceMfaLocalOnly: queriedLogin.ForceMFALocalOnly,
PasswordlessType: policy_pb.PasswordlessType(queriedLogin.PasswordlessType),
HidePasswordReset: queriedLogin.HidePasswordReset,
IgnoreUnknownUsernames: queriedLogin.IgnoreUnknownUsernames,

View File

@@ -14,6 +14,7 @@ func updateLoginPolicyToCommand(p *admin_pb.UpdateLoginPolicyRequest) *command.C
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,

View File

@@ -15,6 +15,7 @@ func AddLoginPolicyToCommand(p *mgmt_pb.AddCustomLoginPolicyRequest) *command.Ad
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
@@ -49,6 +50,7 @@ func updateLoginPolicyToCommand(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *comm
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,

View File

@@ -18,6 +18,7 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
AllowRegister: policy.AllowRegister,
AllowExternalIdp: policy.AllowExternalIDPs,
ForceMfa: policy.ForceMFA,
ForceMfaLocalOnly: policy.ForceMFALocalOnly,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,

View File

@@ -8,7 +8,6 @@ import (
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
// TODO: ?
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
multi := make([]settings.MultiFactorType, len(current.MultiFactors))
for i, typ := range current.MultiFactors {
@@ -24,6 +23,7 @@ func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
AllowRegister: current.AllowRegister,
AllowExternalIdp: current.AllowExternalIDPs,
ForceMfa: current.ForceMFA,
ForceMfaLocalOnly: current.ForceMFALocalOnly,
PasskeysType: passkeysTypeToPb(current.PasswordlessType),
HidePasswordReset: current.HidePasswordReset,
IgnoreUnknownUsernames: current.IgnoreUnknownUsernames,

View File

@@ -25,6 +25,7 @@ func Test_loginSettingsToPb(t *testing.T) {
AllowRegister: true,
AllowExternalIDPs: true,
ForceMFA: true,
ForceMFALocalOnly: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
@@ -52,6 +53,7 @@ func Test_loginSettingsToPb(t *testing.T) {
AllowRegister: true,
AllowExternalIdp: true,
ForceMfa: true,
ForceMfaLocalOnly: true,
PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,

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

View File

@@ -166,16 +166,25 @@ func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMeth
if domain.HasMFA(authMethods) {
return nil
}
availableAuthMethods, forceMFA, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
availableAuthMethods, forceMFA, forceMFALocalOnly, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
if err != nil {
return err
}
if forceMFA || domain.HasMFA(availableAuthMethods) {
if domain.RequiresMFA(forceMFA, forceMFALocalOnly, hasIDPAuthentication(authMethods)) || domain.HasMFA(availableAuthMethods) {
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
}
return nil
}
func hasIDPAuthentication(authMethods []domain.UserAuthMethodType) bool {
for _, method := range authMethods {
if method == domain.UserAuthMethodTypeIDP {
return true
}
}
return false
}
func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType {
types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
if !session.PasswordFactor.PasswordCheckedAt.IsZero() {

View File

@@ -70,6 +70,7 @@ type InstanceSetup struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
ForceMFALocalOnly bool
HidePasswordReset bool
IgnoreUnknownUsername bool
AllowDomainDiscovery bool
@@ -226,6 +227,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LoginPolicy.AllowRegister,
setup.LoginPolicy.AllowExternalIDP,
setup.LoginPolicy.ForceMFA,
setup.LoginPolicy.ForceMFALocalOnly,
setup.LoginPolicy.HidePasswordReset,
setup.LoginPolicy.IgnoreUnknownUsername,
setup.LoginPolicy.AllowDomainDiscovery,

View File

@@ -34,6 +34,7 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy {
IgnoreUnknownUsernames: wm.IgnoreUnknownUsernames,
AllowDomainDiscovery: wm.AllowDomainDiscovery,
ForceMFA: wm.ForceMFA,
ForceMFALocalOnly: wm.ForceMFALocalOnly,
PasswordlessType: wm.PasswordlessType,
DefaultRedirectURI: wm.DefaultRedirectURI,
PasswordCheckLifetime: wm.PasswordCheckLifetime,
@@ -123,10 +124,10 @@ func writeModelToLockoutPolicy(wm *LockoutPolicyWriteModel) *domain.LockoutPolic
func writeModelToPrivacyPolicy(wm *PrivacyPolicyWriteModel) *domain.PrivacyPolicy {
return &domain.PrivacyPolicy{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
TOSLink: wm.TOSLink,
PrivacyLink: wm.PrivacyLink,
HelpLink: wm.HelpLink,
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
TOSLink: wm.TOSLink,
PrivacyLink: wm.PrivacyLink,
HelpLink: wm.HelpLink,
SupportEmail: wm.SupportEmail,
}
}

View File

@@ -234,6 +234,7 @@ func prepareChangeDefaultLoginPolicy(a *instance.Aggregate, policy *ChangeLoginP
policy.AllowRegister,
policy.AllowExternalIDP,
policy.ForceMFA,
policy.ForceMFALocalOnly,
policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.AllowDomainDiscovery,
@@ -260,6 +261,7 @@ func prepareAddDefaultLoginPolicy(
allowRegister bool,
allowExternalIDP bool,
forceMFA bool,
forceMFALocalOnly bool,
hidePasswordReset bool,
ignoreUnknownUsernames bool,
allowDomainDiscovery bool,
@@ -293,6 +295,7 @@ func prepareAddDefaultLoginPolicy(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,

View File

@@ -65,6 +65,7 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -92,6 +93,9 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
if wm.ForceMFA != forceMFA {
changes = append(changes, policy.ChangeForceMFA(forceMFA))
}
if wm.ForceMFALocalOnly != forceMFALocalOnly {
changes = append(changes, policy.ChangeForceMFALocalOnly(forceMFALocalOnly))
}
if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType {
changes = append(changes, policy.ChangePasswordlessType(passwordlessType))
}

View File

@@ -73,6 +73,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -92,6 +93,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -129,6 +131,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -153,6 +156,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*10,
@@ -172,6 +176,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowUsernamePassword: false,
AllowExternalIDP: false,
ForceMFA: false,
ForceMFALocalOnly: false,
HidePasswordReset: false,
IgnoreUnknownUsernames: false,
AllowDomainDiscovery: false,
@@ -281,6 +286,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -322,6 +328,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -383,6 +390,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -526,6 +534,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -567,6 +576,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -621,6 +631,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -680,6 +691,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -747,6 +759,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1301,7 +1314,7 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) {
}
}
func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA,
func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, forceMFALocalOnly,
hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, disableLoginWithEmail, disableLoginWithPhone bool,
passwordlessType domain.PasswordlessType,
redirectURI string,
@@ -1312,6 +1325,7 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow
policy.ChangeAllowRegister(allowRegister),
policy.ChangeAllowExternalIDP(allowExternalIDP),
policy.ChangeForceMFA(forceMFA),
policy.ChangeForceMFALocalOnly(forceMFALocalOnly),
policy.ChangeAllowUserNamePassword(allowUsernamePassword),
policy.ChangeHidePasswordReset(hidePasswordReset),
policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),

View File

@@ -21,6 +21,7 @@ type AddLoginPolicy struct {
AllowExternalIDP bool
IDPProviders []*AddLoginPolicyIDP
ForceMFA bool
ForceMFALocalOnly bool
SecondFactors []domain.SecondFactorType
MultiFactors []domain.MultiFactorType
PasswordlessType domain.PasswordlessType
@@ -47,6 +48,7 @@ type ChangeLoginPolicy struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
ForceMFALocalOnly bool
PasswordlessType domain.PasswordlessType
HidePasswordReset bool
IgnoreUnknownUsernames bool
@@ -425,6 +427,7 @@ func prepareAddLoginPolicy(a *org.Aggregate, policy *AddLoginPolicy) preparation
policy.AllowRegister,
policy.AllowExternalIDP,
policy.ForceMFA,
policy.ForceMFALocalOnly,
policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.AllowDomainDiscovery,

View File

@@ -61,6 +61,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
true,
false,
false,
domain.PasswordlessTypeAllowed,
@@ -83,6 +84,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
@@ -118,6 +120,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -139,6 +142,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -174,6 +178,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -213,6 +218,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -246,6 +252,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -285,6 +292,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -341,6 +349,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -369,6 +378,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -450,6 +460,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
@@ -480,6 +491,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -500,6 +512,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -536,6 +549,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -581,6 +595,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: false,
AllowExternalIDP: false,
ForceMFA: false,
ForceMFALocalOnly: false,
IgnoreUnknownUsernames: false,
AllowDomainDiscovery: false,
DisableLoginWithEmail: false,
@@ -686,6 +701,7 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -829,6 +845,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -873,6 +890,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -937,6 +955,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1104,6 +1123,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1148,6 +1168,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1204,6 +1225,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1267,6 +1289,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1338,6 +1361,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,

View File

@@ -15,6 +15,7 @@ type LoginPolicyWriteModel struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
ForceMFALocalOnly bool
HidePasswordReset bool
IgnoreUnknownUsernames bool
AllowDomainDiscovery bool
@@ -38,6 +39,7 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
wm.AllowUserNamePassword = e.AllowUserNamePassword
wm.AllowExternalIDP = e.AllowExternalIDP
wm.ForceMFA = e.ForceMFA
wm.ForceMFALocalOnly = e.ForceMFALocalOnly
wm.PasswordlessType = e.PasswordlessType
wm.HidePasswordReset = e.HidePasswordReset
wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames
@@ -64,6 +66,9 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
if e.ForceMFA != nil {
wm.ForceMFA = *e.ForceMFA
}
if e.ForceMFALocalOnly != nil {
wm.ForceMFALocalOnly = *e.ForceMFALocalOnly
}
if e.HidePasswordReset != nil {
wm.HidePasswordReset = *e.HidePasswordReset
}

View File

@@ -1182,6 +1182,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1222,6 +1223,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1263,6 +1265,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1320,6 +1323,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1406,6 +1410,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1499,6 +1504,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1582,6 +1588,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1671,6 +1678,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,

View File

@@ -2246,6 +2246,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2315,6 +2316,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2384,6 +2386,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2470,6 +2473,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2614,6 +2618,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2726,6 +2731,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2838,6 +2844,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2944,6 +2951,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -3072,6 +3080,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -3195,6 +3204,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,

View File

@@ -16,6 +16,7 @@ type LoginPolicy struct {
AllowExternalIDP bool
IDPProviders []*IDPProvider
ForceMFA bool
ForceMFALocalOnly bool
SecondFactors []SecondFactorType
MultiFactors []MultiFactorType
PasswordlessType PasswordlessType

View File

@@ -85,6 +85,16 @@ func HasMFA(methods []UserAuthMethodType) bool {
return factors > 1
}
// RequiresMFA checks whether the user requires to authenticate with multiple auth factors based on the LoginPolicy and the authentication type.
// Internal authentication will require MFA if either option is activated.
// External authentication will only require MFA if it's forced generally and not local only.
func RequiresMFA(forceMFA, forceMFALocalOnly, isInternalLogin bool) bool {
if isInternalLogin {
return forceMFA || forceMFALocalOnly
}
return forceMFA && !forceMFALocalOnly
}
type PersonalAccessTokenState int32
const (

View File

@@ -22,7 +22,7 @@ var (
` COUNT(*) OVER ()` +
` FROM projections.idp_login_policy_links5` +
` LEFT JOIN projections.idp_templates5 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates5.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates5.instance_id` +
` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies4 AS login_policy_owner` +
` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies5 AS login_policy_owner` +
` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` +
` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)

View File

@@ -26,6 +26,7 @@ type LoginPolicy struct {
AllowUsernamePassword bool
AllowExternalIDPs bool
ForceMFA bool
ForceMFALocalOnly bool
SecondFactors database.EnumArray[domain.SecondFactorType]
MultiFactors database.EnumArray[domain.MultiFactorType]
PasswordlessType domain.PasswordlessType
@@ -95,6 +96,10 @@ var (
name: projection.LoginPolicyForceMFACol,
table: loginPolicyTable,
}
LoginPolicyColumnForceMFALocalOnly = Column{
name: projection.LoginPolicyForceMFALocalOnlyCol,
table: loginPolicyTable,
}
LoginPolicyColumnSecondFactors = Column{
name: projection.LoginPolicy2FAsCol,
table: loginPolicyTable,
@@ -351,6 +356,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select
LoginPolicyColumnAllowUsernamePassword.identifier(),
LoginPolicyColumnAllowExternalIDPs.identifier(),
LoginPolicyColumnForceMFA.identifier(),
LoginPolicyColumnForceMFALocalOnly.identifier(),
LoginPolicyColumnSecondFactors.identifier(),
LoginPolicyColumnMultiFactors.identifier(),
LoginPolicyColumnPasswordlessType.identifier(),
@@ -381,6 +387,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select
&p.AllowUsernamePassword,
&p.AllowExternalIDPs,
&p.ForceMFA,
&p.ForceMFALocalOnly,
&p.SecondFactors,
&p.MultiFactors,
&p.PasswordlessType,

View File

@@ -15,30 +15,31 @@ import (
)
var (
loginPolicyQuery = `SELECT projections.login_policies4.aggregate_id,` +
` projections.login_policies4.creation_date,` +
` projections.login_policies4.change_date,` +
` projections.login_policies4.sequence,` +
` projections.login_policies4.allow_register,` +
` projections.login_policies4.allow_username_password,` +
` projections.login_policies4.allow_external_idps,` +
` projections.login_policies4.force_mfa,` +
` projections.login_policies4.second_factors,` +
` projections.login_policies4.multi_factors,` +
` projections.login_policies4.passwordless_type,` +
` projections.login_policies4.is_default,` +
` projections.login_policies4.hide_password_reset,` +
` projections.login_policies4.ignore_unknown_usernames,` +
` projections.login_policies4.allow_domain_discovery,` +
` projections.login_policies4.disable_login_with_email,` +
` projections.login_policies4.disable_login_with_phone,` +
` projections.login_policies4.default_redirect_uri,` +
` projections.login_policies4.password_check_lifetime,` +
` projections.login_policies4.external_login_check_lifetime,` +
` projections.login_policies4.mfa_init_skip_lifetime,` +
` projections.login_policies4.second_factor_check_lifetime,` +
` projections.login_policies4.multi_factor_check_lifetime` +
` FROM projections.login_policies4` +
loginPolicyQuery = `SELECT projections.login_policies5.aggregate_id,` +
` projections.login_policies5.creation_date,` +
` projections.login_policies5.change_date,` +
` projections.login_policies5.sequence,` +
` projections.login_policies5.allow_register,` +
` projections.login_policies5.allow_username_password,` +
` projections.login_policies5.allow_external_idps,` +
` projections.login_policies5.force_mfa,` +
` projections.login_policies5.force_mfa_local_only,` +
` projections.login_policies5.second_factors,` +
` projections.login_policies5.multi_factors,` +
` projections.login_policies5.passwordless_type,` +
` projections.login_policies5.is_default,` +
` projections.login_policies5.hide_password_reset,` +
` projections.login_policies5.ignore_unknown_usernames,` +
` projections.login_policies5.allow_domain_discovery,` +
` projections.login_policies5.disable_login_with_email,` +
` projections.login_policies5.disable_login_with_phone,` +
` projections.login_policies5.default_redirect_uri,` +
` projections.login_policies5.password_check_lifetime,` +
` projections.login_policies5.external_login_check_lifetime,` +
` projections.login_policies5.mfa_init_skip_lifetime,` +
` projections.login_policies5.second_factor_check_lifetime,` +
` projections.login_policies5.multi_factor_check_lifetime` +
` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
loginPolicyCols = []string{
"aggregate_id",
@@ -49,6 +50,7 @@ var (
"allow_username_password",
"allow_external_idps",
"force_mfa",
"force_mfa_local_only",
"second_factors",
"multi_factors",
"passwordless_type",
@@ -66,15 +68,15 @@ var (
"multi_factor_check_lifetime",
}
prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies4.second_factors` +
` FROM projections.login_policies4` +
prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies5.second_factors` +
` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
prepareLoginPolicy2FAsCols = []string{
"second_factors",
}
prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies4.multi_factors` +
` FROM projections.login_policies4` +
prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies5.multi_factors` +
` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
prepareLoginPolicyMFAsCols = []string{
"multi_factors",
@@ -126,6 +128,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
true,
true,
true,
true,
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
domain.PasswordlessTypeAllowed,
@@ -153,6 +156,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDPs: true,
ForceMFA: true,
ForceMFALocalOnly: true,
SecondFactors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
MultiFactors: database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
PasswordlessType: domain.PasswordlessTypeAllowed,

View File

@@ -13,7 +13,7 @@ import (
)
const (
LoginPolicyTable = "projections.login_policies4"
LoginPolicyTable = "projections.login_policies5"
LoginPolicyIDCol = "aggregate_id"
LoginPolicyInstanceIDCol = "instance_id"
@@ -25,6 +25,7 @@ const (
LoginPolicyAllowUsernamePasswordCol = "allow_username_password"
LoginPolicyAllowExternalIDPsCol = "allow_external_idps"
LoginPolicyForceMFACol = "force_mfa"
LoginPolicyForceMFALocalOnlyCol = "force_mfa_local_only"
LoginPolicy2FAsCol = "second_factors"
LoginPolicyMFAsCol = "multi_factors"
LoginPolicyPasswordlessTypeCol = "passwordless_type"
@@ -62,6 +63,7 @@ func newLoginPolicyProjection(ctx context.Context, config crdb.StatementHandlerC
crdb.NewColumn(LoginPolicyAllowUsernamePasswordCol, crdb.ColumnTypeBool),
crdb.NewColumn(LoginPolicyAllowExternalIDPsCol, crdb.ColumnTypeBool),
crdb.NewColumn(LoginPolicyForceMFACol, crdb.ColumnTypeBool),
crdb.NewColumn(LoginPolicyForceMFALocalOnlyCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(LoginPolicy2FAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyMFAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum),
@@ -185,6 +187,7 @@ func (p *loginPolicyProjection) reduceLoginPolicyAdded(event eventstore.Event) (
handler.NewCol(LoginPolicyAllowUsernamePasswordCol, policyEvent.AllowUserNamePassword),
handler.NewCol(LoginPolicyAllowExternalIDPsCol, policyEvent.AllowExternalIDP),
handler.NewCol(LoginPolicyForceMFACol, policyEvent.ForceMFA),
handler.NewCol(LoginPolicyForceMFALocalOnlyCol, policyEvent.ForceMFALocalOnly),
handler.NewCol(LoginPolicyPasswordlessTypeCol, policyEvent.PasswordlessType),
handler.NewCol(LoginPolicyIsDefaultCol, isDefault),
handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset),
@@ -228,6 +231,9 @@ func (p *loginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event)
if policyEvent.ForceMFA != nil {
cols = append(cols, handler.NewCol(LoginPolicyForceMFACol, *policyEvent.ForceMFA))
}
if policyEvent.ForceMFALocalOnly != nil {
cols = append(cols, handler.NewCol(LoginPolicyForceMFALocalOnlyCol, *policyEvent.ForceMFALocalOnly))
}
if policyEvent.PasswordlessType != nil {
cols = append(cols, handler.NewCol(LoginPolicyPasswordlessTypeCol, *policyEvent.PasswordlessType))
}

View File

@@ -24,7 +24,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
want wantReduce
}{
{
name: "org reduceLoginPolicyAdded",
name: "org reduceLoginPolicyAdded without forceMFALocalOnly",
args: args{
event: getEvent(testEvent(
repository.EventType(org.LoginPolicyAddedEventType),
@@ -57,7 +57,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.login_policies4 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -68,6 +68,73 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
false,
false,
false,
domain.PasswordlessTypeAllowed,
false,
true,
true,
true,
true,
true,
"https://example.com/redirect",
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
},
},
},
},
},
},
{
name: "org reduceLoginPolicyAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.LoginPolicyAddedEventType),
org.AggregateType,
[]byte(`{
"allowUsernamePassword": true,
"allowRegister": true,
"allowExternalIdp": true,
"forceMFA": true,
"forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
"disableLoginWithEmail": true,
"disableLoginWithPhone": true,
"passwordlessType": 1,
"defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000,
"secondFactorCheckLifetime": 10000000,
"multiFactorCheckLifetime": 10000000
}`),
), org.LoginPolicyAddedEventMapper),
},
reduce: (&loginPolicyProjection{}).reduceLoginPolicyAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
anyArg{},
anyArg{},
uint64(15),
true,
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
false,
true,
@@ -99,6 +166,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowRegister": true,
"allowExternalIdp": true,
"forceMFA": true,
"forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -121,7 +189,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) WHERE (aggregate_id = $19) AND (instance_id = $20)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) WHERE (aggregate_id = $20) AND (instance_id = $21)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -129,6 +197,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -168,7 +237,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -200,7 +269,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -230,7 +299,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.login_policies4 WHERE (aggregate_id = $1) AND (instance_id = $2)",
expectedStmt: "DELETE FROM projections.login_policies5 WHERE (aggregate_id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -259,7 +328,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -291,7 +360,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -314,8 +383,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
[]byte(`{
"allowUsernamePassword": true,
"allowRegister": true,
"allowExternalIdp": false,
"forceMFA": false,
"allowExternalIdp": true,
"forceMFA": true,
"forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -338,7 +408,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.login_policies4 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -347,8 +417,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
uint64(15),
true,
true,
false,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -380,6 +451,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowRegister": true,
"allowExternalIdp": true,
"forceMFA": true,
"forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -397,7 +469,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE (aggregate_id = $14) AND (instance_id = $15)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) WHERE (aggregate_id = $15) AND (instance_id = $16)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -405,6 +477,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -439,7 +512,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -471,7 +544,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -503,7 +576,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -535,7 +608,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -565,7 +638,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (aggregate_id = $5)",
expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (aggregate_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -595,7 +668,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.login_policies4 WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.login_policies5 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@@ -80,11 +80,12 @@ var (
table: userIDPsCountTable,
}
forceMFATable = loginPolicyTable.setAlias("auth_methods_force_mfa")
forceMFAInstanceID = LoginPolicyColumnInstanceID.setTable(forceMFATable)
forceMFAOrgID = LoginPolicyColumnOrgID.setTable(forceMFATable)
forceMFAIsDefault = LoginPolicyColumnIsDefault.setTable(forceMFATable)
forceMFAForce = LoginPolicyColumnForceMFA.setTable(forceMFATable)
forceMFATable = loginPolicyTable.setAlias("auth_methods_force_mfa")
forceMFAInstanceID = LoginPolicyColumnInstanceID.setTable(forceMFATable)
forceMFAOrgID = LoginPolicyColumnOrgID.setTable(forceMFATable)
forceMFAIsDefault = LoginPolicyColumnIsDefault.setTable(forceMFATable)
forceMFAForce = LoginPolicyColumnForceMFA.setTable(forceMFATable)
forceMFAForceLocalOnly = LoginPolicyColumnForceMFALocalOnly.setTable(forceMFATable)
)
type AuthMethods struct {
@@ -176,11 +177,11 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri
return userAuthMethodTypes, err
}
func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA bool, err error) {
func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error) {
ctxData := authz.GetCtxData(ctx)
if ctxData.UserID != userID {
if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
return nil, false, err
return nil, false, false, err
}
}
ctx, span := tracing.NewSpan(ctx)
@@ -196,12 +197,12 @@ func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID st
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, false, errors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
return nil, false, false, errors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil || rows.Err() != nil {
return nil, false, errors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal")
return nil, false, false, errors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal")
}
return scan(rows)
}
@@ -408,7 +409,7 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
}
}
func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]domain.UserAuthMethodType, bool, error)) {
func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (_ []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error)) {
loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery()
if err != nil {
return sq.SelectBuilder{}, nil
@@ -425,7 +426,8 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData
NotifyPasswordSetCol.identifier(),
authMethodTypeTypes.identifier(),
userIDPsCountCount.identifier(),
forceMFAForce.identifier()).
forceMFAForce.identifier(),
forceMFAForceLocalOnly.identifier()).
From(userTable.identifier()).
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
@@ -439,11 +441,12 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData
"(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " +
forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, error) {
func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, bool, error) {
userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
var passwordSet sql.NullBool
var idp sql.NullInt64
var forceMFA sql.NullBool
var forceMFALocalOnly sql.NullBool
for rows.Next() {
var authMethodType sql.NullInt16
err := rows.Scan(
@@ -451,9 +454,10 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData
&authMethodType,
&idp,
&forceMFA,
&forceMFALocalOnly,
)
if err != nil {
return nil, false, err
return nil, false, false, err
}
if authMethodType.Valid {
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
@@ -468,10 +472,10 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData
}
if err := rows.Close(); err != nil {
return nil, false, errors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
return nil, false, false, errors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
}
return userAuthMethodTypes, forceMFA.Bool, nil
return userAuthMethodTypes, forceMFA.Bool, forceMFALocalOnly.Bool, nil
}
}
@@ -502,6 +506,7 @@ func prepareAuthMethodQuery() (string, []interface{}, error) {
func prepareAuthMethodsForceMFAQuery() (string, error) {
loginPolicyQuery, _, err := sq.Select(
forceMFAForce.identifier(),
forceMFAForceLocalOnly.identifier(),
forceMFAInstanceID.identifier(),
forceMFAOrgID.identifier(),
).

View File

@@ -59,7 +59,8 @@ var (
prepareAuthMethodTypesRequiredStmt = `SELECT projections.users8_notifications.password_set,` +
` auth_method_types.method_type,` +
` user_idps_count.count,` +
` auth_methods_force_mfa.force_mfa` +
` auth_methods_force_mfa.force_mfa,` +
` auth_methods_force_mfa.force_mfa_local_only` +
` FROM projections.users8` +
` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_notifications.instance_id` +
` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
@@ -68,7 +69,7 @@ var (
` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
` ON user_idps_count.user_id = projections.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies4 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` +
` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.force_mfa_local_only, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies5 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` +
` ON (auth_methods_force_mfa.aggregate_id = projections.users8.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users8.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms
`
@@ -77,6 +78,7 @@ var (
"method_type",
"idps_count",
"force_mfa",
"force_mfa_local_only",
}
)
@@ -318,11 +320,11 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
}
},
want: want{
@@ -339,11 +341,11 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
}
},
want: want{
@@ -356,6 +358,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
domain.UserAuthMethodTypePasswordless,
1,
true,
true,
},
},
),
@@ -366,7 +369,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
forceMFA: true,
forceMFA: true,
forceMFALocalOnly: true,
},
},
{
@@ -374,11 +378,11 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
}
},
want: want{
@@ -391,12 +395,14 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
domain.UserAuthMethodTypePasswordless,
1,
true,
true,
},
{
true,
domain.UserAuthMethodTypeOTP,
1,
true,
true,
},
},
),
@@ -409,7 +415,8 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
forceMFA: true,
forceMFA: true,
forceMFALocalOnly: true,
},
},
{
@@ -417,11 +424,11 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
}
},
want: want{
@@ -448,6 +455,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
// testUserAuthMethodTypesRequired is required as assetPrepare is only able to return a single object from scan
type testUserAuthMethodTypesRequired struct {
authMethods []domain.UserAuthMethodType
forceMFA bool
authMethods []domain.UserAuthMethodType
forceMFA bool
forceMFALocalOnly bool
}

View File

@@ -27,6 +27,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -50,6 +51,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,

View File

@@ -28,6 +28,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -51,6 +52,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,

View File

@@ -24,6 +24,7 @@ type LoginPolicyAddedEvent struct {
AllowRegister bool `json:"allowRegister,omitempty"`
AllowExternalIDP bool `json:"allowExternalIdp,omitempty"`
ForceMFA bool `json:"forceMFA,omitempty"`
ForceMFALocalOnly bool `json:"forceMFALocalOnly,omitempty"`
HidePasswordReset bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"`
AllowDomainDiscovery bool `json:"allowDomainDiscovery,omitempty"`
@@ -52,6 +53,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -71,6 +73,7 @@ func NewLoginPolicyAddedEvent(
AllowRegister: allowRegister,
AllowUserNamePassword: allowUserNamePassword,
ForceMFA: forceMFA,
ForceMFALocalOnly: forceMFALocalOnly,
PasswordlessType: passwordlessType,
HidePasswordReset: hidePasswordReset,
IgnoreUnknownUsernames: ignoreUnknownUsernames,
@@ -106,6 +109,7 @@ type LoginPolicyChangedEvent struct {
AllowRegister *bool `json:"allowRegister,omitempty"`
AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"`
ForceMFA *bool `json:"forceMFA,omitempty"`
ForceMFALocalOnly *bool `json:"forceMFALocalOnly,omitempty"`
HidePasswordReset *bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"`
AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"`
@@ -170,6 +174,12 @@ func ChangeForceMFA(forceMFA bool) func(*LoginPolicyChangedEvent) {
}
}
func ChangeForceMFALocalOnly(forceMFALocalOnly bool) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.ForceMFALocalOnly = &forceMFALocalOnly
}
}
func ChangePasswordlessType(passwordlessType domain.PasswordlessType) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.PasswordlessType = &passwordlessType

View File

@@ -170,12 +170,12 @@ func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.L
return types
}
func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy) ([]domain.MFAType, bool) {
func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy, isInternalAuthentication bool) ([]domain.MFAType, bool) {
types := make([]domain.MFAType, 0)
required := true
switch level {
default:
required = policy.ForceMFA
required = domain.RequiresMFA(policy.ForceMFA, policy.ForceMFALocalOnly, isInternalAuthentication)
fallthrough
case domain.MFALevelSecondFactor:
if policy.HasSecondFactors() {