fix(iam): actually enforce MFA

forceMFA never took effect because of the JSON key. ZITADEL's v1 endpoint speaks
protojson, which accepts "forceMfa" (lowerCamel of force_mfa). The spelling
"forceMFA" is treated as an unknown field and silently dropped, so every earlier
run returned 200 with a new sequence while force_mfa stayed false in
projections.login_policies5.

Send forceMfa and forceMfaLocalOnly. Verified in the database:

  force_mfa = t | force_mfa_local_only = t | mfa_init_skip_lifetime = 0

and in the login flow: after the password step the browser lands on
/ui/login/mfa/prompt with the "2-Factor Setup" page, so a second factor is now
required instead of optional.
This commit is contained in:
2026-10-27 00:14:42 -07:00
parent 6768d1c762
commit cc7c30ee20
+8 -3
View File
@@ -420,15 +420,20 @@ def _ensure_smtp(token: str) -> str:
def _ensure_login_policy(token: str) -> None: def _ensure_login_policy(token: str) -> None:
"""Require password + a second factor, and demand enrolment at login. """Require password + a second factor, and demand enrolment at login.
forceMFA does not persist through the v1 policy endpoint (it reads back Enforcement needs forceMfa. The v1 endpoint ignores the spelling "forceMFA"
absent), so the effective lever is mfaInitSkipLifetime: 0s, which makes the as an unknown field, which is why earlier runs looked successful while
login flow require enrolment instead of letting a user skip it for 30 days. projections.login_policies5 kept force_mfa = false.
Second factors stay OTP and U2F: an authenticator app or a hardware key. Second factors stay OTP and U2F: an authenticator app or a hardware key.
Multi-factor is left configured for U2F with verification so a later step Multi-factor is left configured for U2F with verification so a later step
can move 2FA to 2MFA without another policy change. can move 2FA to 2MFA without another policy change.
""" """
code, d = _api("PUT", "/admin/v1/policies/login", token, { code, d = _api("PUT", "/admin/v1/policies/login", token, {
# Case matters: protojson accepts "forceMfa", the lowerCamel form of
# force_mfa. Sending "forceMFA" is silently ignored as an unknown field,
# so the policy looks unchanged and MFA is never enforced.
"forceMfa": True,
"forceMfaLocalOnly": True,
"allowUsernamePassword": True, "allowUsernamePassword": True,
"allowRegister": False, "allowRegister": False,
"allowExternalIDP": True, "allowExternalIDP": True,