portal: stop forcing MFA on first login

This commit is contained in:
Brad Stein 2026-01-04 08:21:28 -03:00
parent 632cb9c17b
commit 59830e19c8
3 changed files with 26 additions and 41 deletions

View File

@ -204,7 +204,10 @@ def provision_access_request(request_code: str) -> ProvisionResult:
raise RuntimeError("email is already associated with an existing Atlas account") raise RuntimeError("email is already associated with an existing Atlas account")
# Always enforce email verification in Keycloak itself (even if the portal # Always enforce email verification in Keycloak itself (even if the portal
# already verified an external email before approval). # already verified an external email before approval).
required_actions = ["UPDATE_PASSWORD", "VERIFY_EMAIL", "CONFIGURE_TOTP"] # Do not force MFA enrollment during initial login: some users prefer to
# enable MFA later and some clients are too friction-heavy when MFA is
# mandatory for every service.
required_actions = ["UPDATE_PASSWORD", "VERIFY_EMAIL"]
payload = { payload = {
"username": username, "username": username,
"enabled": True, "enabled": True,
@ -222,6 +225,11 @@ def provision_access_request(request_code: str) -> ProvisionResult:
try: try:
full = admin_client().get_user(user_id) full = admin_client().get_user(user_id)
attrs = full.get("attributes") or {} attrs = full.get("attributes") or {}
actions = full.get("requiredActions")
if isinstance(actions, list) and "CONFIGURE_TOTP" in actions:
# Backfill earlier accounts created when we forced MFA enrollment.
new_actions = [a for a in actions if a != "CONFIGURE_TOTP"]
admin_client().update_user(user_id, {"requiredActions": new_actions})
mailu_from_attr: str | None = None mailu_from_attr: str | None = None
if isinstance(attrs, dict): if isinstance(attrs, dict):
raw_mailu = attrs.get(MAILU_EMAIL_ATTR) raw_mailu = attrs.get(MAILU_EMAIL_ATTR)

View File

@ -58,14 +58,12 @@ def _verify_url(request_code: str, token: str) -> str:
ONBOARDING_STEPS: tuple[str, ...] = ( ONBOARDING_STEPS: tuple[str, ...] = (
"keycloak_password_changed", "keycloak_password_changed",
"keycloak_mfa_configured",
"vaultwarden_master_password", "vaultwarden_master_password",
"element_recovery_key", "element_recovery_key",
"element_recovery_key_stored", "element_recovery_key_stored",
) )
KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed", "keycloak_mfa_configured"} KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed"}
KEYCLOAK_OTP_CRED_TYPES: set[str] = {"otp", "totp"}
def _normalize_status(status: str) -> str: def _normalize_status(status: str) -> str:
@ -109,25 +107,25 @@ def _auto_completed_keycloak_steps(username: str) -> set[str]:
actions = full.get("requiredActions") actions = full.get("requiredActions")
required_actions: set[str] = set() required_actions: set[str] = set()
actions_list: list[str] = []
if isinstance(actions, list): if isinstance(actions, list):
required_actions = {a for a in actions if isinstance(a, str)} actions_list = [a for a in actions if isinstance(a, str)]
required_actions = set(actions_list)
if "UPDATE_PASSWORD" not in required_actions: if "UPDATE_PASSWORD" not in required_actions:
completed.add("keycloak_password_changed") completed.add("keycloak_password_changed")
otp_present = False # Backfill: earlier accounts were created with CONFIGURE_TOTP as a required action,
# which forces users to enroll MFA at first login. We no longer require that, so
# remove it if present.
if "CONFIGURE_TOTP" in required_actions:
try: try:
creds = admin_client().get_user_credentials(user_id) admin_client().update_user(
for cred in creds: user_id,
ctype = cred.get("type") if isinstance(cred, dict) else None {"requiredActions": [a for a in actions_list if a != "CONFIGURE_TOTP"]},
if isinstance(ctype, str) and ctype.lower() in KEYCLOAK_OTP_CRED_TYPES: )
otp_present = True
break
except Exception: except Exception:
otp_present = False pass
if otp_present or "CONFIGURE_TOTP" not in required_actions:
completed.add("keycloak_mfa_configured")
except Exception: except Exception:
return set() return set()

View File

@ -81,7 +81,7 @@
</div> </div>
<p class="muted"> <p class="muted">
Some steps are verified automatically from Keycloak (password + MFA). Others can't be verified yet — mark them complete once you're done. Some steps are verified automatically from Keycloak (password). Others can't be verified yet — mark them complete once you're done.
</p> </p>
<div v-if="initialPassword" class="initial-password"> <div v-if="initialPassword" class="initial-password">
@ -131,27 +131,6 @@
</p> </p>
</li> </li>
<li class="check-item">
<label>
<input
type="checkbox"
:checked="isStepDone('keycloak_mfa_configured')"
disabled
/>
<span>Enable Keycloak MFA</span>
<span
class="pill mono auto-pill"
:class="isStepDone('keycloak_mfa_configured') ? 'pill-ok' : 'pill-warn'"
>
{{ isStepDone("keycloak_mfa_configured") ? "verified" : "pending" }}
</span>
</label>
<p class="muted">
Add a TOTP authenticator (2FA) in Keycloak:
<a href="https://sso.bstein.dev/realms/atlas/account" target="_blank" rel="noreferrer">account console</a>.
</p>
</li>
<li class="check-item"> <li class="check-item">
<label> <label>
<input <input
@ -335,7 +314,7 @@ async function toggleStep(step, event) {
event?.preventDefault?.(); event?.preventDefault?.();
return; return;
} }
if (step === "keycloak_password_changed" || step === "keycloak_mfa_configured") { if (step === "keycloak_password_changed") {
event?.preventDefault?.(); event?.preventDefault?.();
return; return;
} }