diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index 82a006483..7fd7e2627 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -99,6 +99,28 @@ {{ 'POLICY.DATA.FORCEMFA' | translate }} +
+ + {{ 'POLICY.DATA.FORCEMFALOCALONLY' | translate }} + +
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 ( diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 530780160..22f07e7fa 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -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'`) diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go index 4862aa765..dcf168d94 100644 --- a/internal/query/login_policy.go +++ b/internal/query/login_policy.go @@ -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, diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go index 144545633..2e1900f21 100644 --- a/internal/query/login_policy_test.go +++ b/internal/query/login_policy_test.go @@ -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, diff --git a/internal/query/projection/login_policy.go b/internal/query/projection/login_policy.go index 601a4531b..77fadc2ba 100644 --- a/internal/query/projection/login_policy.go +++ b/internal/query/projection/login_policy.go @@ -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)) } diff --git a/internal/query/projection/login_policy_test.go b/internal/query/projection/login_policy_test.go index 8ade083c0..bd87c5c4b 100644 --- a/internal/query/projection/login_policy_test.go +++ b/internal/query/projection/login_policy_test.go @@ -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", }, diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 46697c61d..fc80685bc 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -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(), ). diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 01f6c90df..ec38d04e5 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -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 } diff --git a/internal/repository/instance/policy_login.go b/internal/repository/instance/policy_login.go index fad06fa71..73a0b3ef4 100644 --- a/internal/repository/instance/policy_login.go +++ b/internal/repository/instance/policy_login.go @@ -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, diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go index 449e3e10a..95e38ca92 100644 --- a/internal/repository/org/policy_login.go +++ b/internal/repository/org/policy_login.go @@ -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, diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go index dfd2d0afd..790b1a571 100644 --- a/internal/repository/policy/login.go +++ b/internal/repository/policy/login.go @@ -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 diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go index 672e18aef..8cb0d4346 100644 --- a/internal/user/model/user_view.go +++ b/internal/user/model/user_view.go @@ -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() { diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index e50d3afb3..d59882770 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -5866,6 +5866,11 @@ message UpdateLoginPolicyRequest { description: "defines if the user can additionally (to the login name) be identified by their verified phone number" } ]; + bool force_mfa_local_only = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; } message UpdateLoginPolicyResponse { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index f5a48a27e..4f434e6fe 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -9654,6 +9654,11 @@ message AddCustomLoginPolicyRequest { description: "defines if the user can additionally (to the login name) be identified by their verified phone number" } ]; + bool force_mfa_local_only = 20 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; } message AddCustomLoginPolicyResponse { @@ -9698,6 +9703,11 @@ message UpdateCustomLoginPolicyRequest { description: "defines if the user can additionally (to the login name) be identified by their verified phone number" } ]; + bool force_mfa_local_only = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; } message UpdateCustomLoginPolicyResponse { diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index c6cbc7a46..49dbe34dc 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -239,6 +239,11 @@ message LoginPolicy { description: "defines if the user can additionally (to the login name) be identified by their verified phone number" } ]; + bool force_mfa_local_only = 22 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; } enum SecondFactorType { diff --git a/proto/zitadel/settings/v2alpha/login_settings.proto b/proto/zitadel/settings/v2alpha/login_settings.proto index d84423c3c..9d4b37237 100644 --- a/proto/zitadel/settings/v2alpha/login_settings.proto +++ b/proto/zitadel/settings/v2alpha/login_settings.proto @@ -104,6 +104,11 @@ message LoginSettings { description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; } ]; + bool force_mfa_local_only = 22 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; } enum SecondFactorType {