mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(entra): add new check entra_admin_users_phishing_resistant_mfa_enabled (#7211)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
committed by
GitHub
parent
b8ce09ec34
commit
7ba99f22cd
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "microsoft365",
|
||||
"CheckID": "entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"CheckTitle": "Ensure phishing-resistant MFA strength is required for all administrator accounts",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"Description": "Ensure ",
|
||||
"Risk": "Administrators using weaker MFA methods, such as SMS or push notifications, are vulnerable to phishing attacks and MFA fatigue attacks. Attackers can intercept codes or trick users into approving fraudulent authentication requests, leading to unauthorized access to critical systems.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles listed below in this section of the document. Under Target resources include All cloud apps and do not create any exclusions. Under Grant select Grant Access and check Require authentication strength and set Phishing-resistant MFA in the dropdown box. Click Select. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Require phishing-resistant MFA strength for all administrator accounts through Conditional Access policies. Enforce the use of FIDO2 security keys, Windows Hello for Business, or certificate-based authentication. Ensure administrators are pre-registered for these methods before enforcement to prevent lockouts. Maintain a break-glass account exempt from this policy for emergency access.",
|
||||
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa#create-a-conditional-access-policy"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
from prowler.lib.check.models import Check, CheckReportMicrosoft365
|
||||
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
AdminRoles,
|
||||
AuthenticationStrength,
|
||||
ConditionalAccessPolicyState,
|
||||
)
|
||||
|
||||
|
||||
class entra_admin_users_phishing_resistant_mfa_enabled(Check):
|
||||
"""Check if Conditional Access policies require Phishing-resistant MFA strength for admin users."""
|
||||
|
||||
def execute(self) -> list[CheckReportMicrosoft365]:
|
||||
"""Execute the check to ensure that Conditional Access policies require Phishing-resistant MFA strength for admin users.
|
||||
|
||||
Returns:
|
||||
list[CheckReportMicrosoft365]: A list containing the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
report = CheckReportMicrosoft365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Conditional Access Policies",
|
||||
resource_id="conditionalAccessPolicies",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
|
||||
|
||||
for policy in entra_client.conditional_access_policies.values():
|
||||
if policy.state == ConditionalAccessPolicyState.DISABLED:
|
||||
continue
|
||||
|
||||
if not (
|
||||
{role.value for role in AdminRoles}.issuperset(
|
||||
policy.conditions.user_conditions.included_roles
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
"All"
|
||||
not in policy.conditions.application_conditions.included_applications
|
||||
or policy.conditions.application_conditions.excluded_applications != []
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
policy.grant_controls.authentication_strength is not None
|
||||
and policy.grant_controls.authentication_strength
|
||||
== AuthenticationStrength.PHISHING_RESISTANT_MFA
|
||||
):
|
||||
report = CheckReportMicrosoft365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.display_name,
|
||||
resource_id=policy.id,
|
||||
)
|
||||
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports Phishing-resistant MFA strength for admin users but does not require it."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires Phishing-resistant MFA strength for admin users."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -214,6 +214,15 @@ class Entra(Microsoft365Service):
|
||||
getattr(policy.grant_controls, "operator", "AND")
|
||||
)
|
||||
),
|
||||
authentication_strength=(
|
||||
AuthenticationStrength(
|
||||
policy.grant_controls.authentication_strength.display_name
|
||||
)
|
||||
if policy.grant_controls is not None
|
||||
and policy.grant_controls.authentication_strength
|
||||
is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
@@ -413,9 +422,16 @@ class GrantControlOperator(Enum):
|
||||
OR = "OR"
|
||||
|
||||
|
||||
class AuthenticationStrength(Enum):
|
||||
MFA = "Multifactor authentication"
|
||||
PASSWORDLESS_MFA = "Passwordless MFA"
|
||||
PHISHING_RESISTANT_MFA = "Phishing-resistant MFA"
|
||||
|
||||
|
||||
class GrantControls(BaseModel):
|
||||
built_in_controls: List[ConditionalAccessGrantControl]
|
||||
operator: GrantControlOperator
|
||||
authentication_strength: Optional[AuthenticationStrength]
|
||||
|
||||
|
||||
class ConditionalAccessPolicy(BaseModel):
|
||||
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
ApplicationsConditions,
|
||||
AuthenticationStrength,
|
||||
ConditionalAccessGrantControl,
|
||||
ConditionalAccessPolicyState,
|
||||
Conditions,
|
||||
GrantControlOperator,
|
||||
GrantControls,
|
||||
PersistentBrowser,
|
||||
SessionControls,
|
||||
SignInFrequency,
|
||||
SignInFrequencyInterval,
|
||||
UsersConditions,
|
||||
)
|
||||
from tests.providers.microsoft365.microsoft365_fixtures import (
|
||||
DOMAIN,
|
||||
set_mocked_microsoft365_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_entra_admin_users_phishing_resistant_mfa_enabled:
|
||||
def test_entra_no_conditional_access_policies(self):
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_microsoft365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
|
||||
entra_admin_users_phishing_resistant_mfa_enabled,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {}
|
||||
|
||||
check = entra_admin_users_phishing_resistant_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
assert result[0].resource_id == "conditionalAccessPolicies"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_entra_phishing_resistant_mfa_strength_disabled(self):
|
||||
id = str(uuid4())
|
||||
display_name = "Test"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_microsoft365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
|
||||
entra_admin_users_phishing_resistant_mfa_enabled,
|
||||
)
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
id: ConditionalAccessPolicy(
|
||||
id=id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[
|
||||
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
|
||||
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
|
||||
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
|
||||
"158c047a-c907-4556-b7ef-446551a6b5f7",
|
||||
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
|
||||
"29232cdf-9323-42fd-ade2-1d097af3e4de",
|
||||
"62e90394-69f5-4237-9190-012177145e10",
|
||||
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
|
||||
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
|
||||
"966707d0-3269-4727-9be2-8c3a10f19b9d",
|
||||
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
|
||||
"e8611ab8-c189-46e8-94e1-60213ab1f814",
|
||||
"194ae4cb-b126-40b2-bd5b-6091b380977d",
|
||||
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
|
||||
"fe930be7-5e62-47db-91af-98c3a49a38b1",
|
||||
],
|
||||
excluded_roles=[],
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
operator=GrantControlOperator.AND,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
is_enabled=False, mode="always"
|
||||
),
|
||||
sign_in_frequency=SignInFrequency(
|
||||
is_enabled=False,
|
||||
frequency=None,
|
||||
type=None,
|
||||
interval=SignInFrequencyInterval.EVERY_TIME,
|
||||
),
|
||||
),
|
||||
state=ConditionalAccessPolicyState.DISABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_admin_users_phishing_resistant_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
assert result[0].resource_id == "conditionalAccessPolicies"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_entra_phishing_resistant_mfa_strength_enabled_for_reporting(self):
|
||||
id = str(uuid4())
|
||||
display_name = "Test"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_microsoft365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
|
||||
entra_admin_users_phishing_resistant_mfa_enabled,
|
||||
)
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
id: ConditionalAccessPolicy(
|
||||
id=id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[
|
||||
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
|
||||
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
|
||||
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
|
||||
"158c047a-c907-4556-b7ef-446551a6b5f7",
|
||||
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
|
||||
"29232cdf-9323-42fd-ade2-1d097af3e4de",
|
||||
"62e90394-69f5-4237-9190-012177145e10",
|
||||
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
|
||||
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
|
||||
"966707d0-3269-4727-9be2-8c3a10f19b9d",
|
||||
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
|
||||
"e8611ab8-c189-46e8-94e1-60213ab1f814",
|
||||
"194ae4cb-b126-40b2-bd5b-6091b380977d",
|
||||
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
|
||||
"fe930be7-5e62-47db-91af-98c3a49a38b1",
|
||||
],
|
||||
excluded_roles=[],
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
operator=GrantControlOperator.AND,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
is_enabled=False, mode="always"
|
||||
),
|
||||
sign_in_frequency=SignInFrequency(
|
||||
is_enabled=False,
|
||||
frequency=None,
|
||||
type=None,
|
||||
interval=SignInFrequencyInterval.EVERY_TIME,
|
||||
),
|
||||
),
|
||||
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_admin_users_phishing_resistant_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' reports Phishing-resistant MFA strength for admin users but does not require it."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.conditional_access_policies[id].dict()
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == id
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_entra_phishing_resistant_mfa_strength_enabled(self):
|
||||
id = str(uuid4())
|
||||
display_name = "Test"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_microsoft365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
|
||||
entra_admin_users_phishing_resistant_mfa_enabled,
|
||||
)
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
ConditionalAccessPolicy,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
id: ConditionalAccessPolicy(
|
||||
id=id,
|
||||
display_name=display_name,
|
||||
conditions=Conditions(
|
||||
application_conditions=ApplicationsConditions(
|
||||
included_applications=["All"],
|
||||
excluded_applications=[],
|
||||
included_user_actions=[],
|
||||
),
|
||||
user_conditions=UsersConditions(
|
||||
included_groups=[],
|
||||
excluded_groups=[],
|
||||
included_users=["All"],
|
||||
excluded_users=[],
|
||||
included_roles=[
|
||||
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
|
||||
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
|
||||
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
|
||||
"158c047a-c907-4556-b7ef-446551a6b5f7",
|
||||
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
|
||||
"29232cdf-9323-42fd-ade2-1d097af3e4de",
|
||||
"62e90394-69f5-4237-9190-012177145e10",
|
||||
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
|
||||
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
|
||||
"966707d0-3269-4727-9be2-8c3a10f19b9d",
|
||||
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
|
||||
"e8611ab8-c189-46e8-94e1-60213ab1f814",
|
||||
"194ae4cb-b126-40b2-bd5b-6091b380977d",
|
||||
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
|
||||
"fe930be7-5e62-47db-91af-98c3a49a38b1",
|
||||
],
|
||||
excluded_roles=[],
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
operator=GrantControlOperator.AND,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
is_enabled=False, mode="always"
|
||||
),
|
||||
sign_in_frequency=SignInFrequency(
|
||||
is_enabled=False,
|
||||
frequency=None,
|
||||
type=None,
|
||||
interval=SignInFrequencyInterval.EVERY_TIME,
|
||||
),
|
||||
),
|
||||
state=ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_admin_users_phishing_resistant_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' requires Phishing-resistant MFA strength for admin users."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.conditional_access_policies[id].dict()
|
||||
)
|
||||
assert result[0].resource_name == display_name
|
||||
assert result[0].resource_id == id
|
||||
assert result[0].location == "global"
|
||||
@@ -4,6 +4,7 @@ from prowler.providers.microsoft365.models import Microsoft365IdentityInfo
|
||||
from prowler.providers.microsoft365.services.entra.entra_service import (
|
||||
AdminConsentPolicy,
|
||||
ApplicationsConditions,
|
||||
AuthenticationStrength,
|
||||
AuthorizationPolicy,
|
||||
AuthPolicyRoles,
|
||||
ConditionalAccessGrantControl,
|
||||
@@ -70,6 +71,7 @@ async def mock_entra_get_conditional_access_policies(_):
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
@@ -188,6 +190,7 @@ class Test_Entra_Service:
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
session_controls=SessionControls(
|
||||
persistent_browser=PersistentBrowser(
|
||||
|
||||
Reference in New Issue
Block a user