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")
# Always enforce email verification in Keycloak itself (even if the portal
# 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 = {
"username": username,
"enabled": True,
@ -222,6 +225,11 @@ def provision_access_request(request_code: str) -> ProvisionResult:
try:
full = admin_client().get_user(user_id)
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
if isinstance(attrs, dict):
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, ...] = (
"keycloak_password_changed",
"keycloak_mfa_configured",
"vaultwarden_master_password",
"element_recovery_key",
"element_recovery_key_stored",
)
KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed", "keycloak_mfa_configured"}
KEYCLOAK_OTP_CRED_TYPES: set[str] = {"otp", "totp"}
KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed"}
def _normalize_status(status: str) -> str:
@ -109,25 +107,25 @@ def _auto_completed_keycloak_steps(username: str) -> set[str]:
actions = full.get("requiredActions")
required_actions: set[str] = set()
actions_list: list[str] = []
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:
completed.add("keycloak_password_changed")
otp_present = False
try:
creds = admin_client().get_user_credentials(user_id)
for cred in creds:
ctype = cred.get("type") if isinstance(cred, dict) else None
if isinstance(ctype, str) and ctype.lower() in KEYCLOAK_OTP_CRED_TYPES:
otp_present = True
break
except Exception:
otp_present = False
if otp_present or "CONFIGURE_TOTP" not in required_actions:
completed.add("keycloak_mfa_configured")
# 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:
admin_client().update_user(
user_id,
{"requiredActions": [a for a in actions_list if a != "CONFIGURE_TOTP"]},
)
except Exception:
pass
except Exception:
return set()

View File

@ -81,7 +81,7 @@
</div>
<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>
<div v-if="initialPassword" class="initial-password">
@ -131,27 +131,6 @@
</p>
</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">
<label>
<input
@ -335,7 +314,7 @@ async function toggleStep(step, event) {
event?.preventDefault?.();
return;
}
if (step === "keycloak_password_changed" || step === "keycloak_mfa_configured") {
if (step === "keycloak_password_changed") {
event?.preventDefault?.();
return;
}