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 {