diff --git a/app/account_step_up_routes.py b/app/account_step_up_routes.py index 897b167..32cc70a 100644 --- a/app/account_step_up_routes.py +++ b/app/account_step_up_routes.py @@ -30,7 +30,13 @@ current_authentication_session, ) from .ldap_service import LDAPDirectory, LDAPLookupRejected, LDAPUnavailable -from .models import LDAPIdentity, OIDCIdentity, User, WebAuthnCredential, db +from .models import ( + LDAPIdentity, + OIDCIdentity, + User, + WebAuthnCredential, + db, +) from .oidc_service import OIDCStateError from .security_features import feature_is_active from .step_up import ( @@ -45,7 +51,7 @@ consume_account_step_up_grant, recent_strong_assurance, ) -from .totp_service import verify_totp +from .totp_service import disable_totp_mfa, verify_totp from .webauthn_service import ChallengeError, consume_challenge, create_challenge @@ -549,7 +555,7 @@ def disable_account_mfa(): ) except StepUpError: return _error("step_up_required", 403) - user.mfa_enabled = False + disable_totp_mfa(user) db.session.commit() log_security_event( "ACCOUNT_STEP_UP_CONSUMED", diff --git a/app/auth_assurance.py b/app/auth_assurance.py index 798b212..db3ae0b 100644 --- a/app/auth_assurance.py +++ b/app/auth_assurance.py @@ -25,6 +25,7 @@ db, ) from .session_epoch import current_epoch +from .totp_service import disable_totp_mfa _PENDING_TTL = timedelta(minutes=5) @@ -497,7 +498,7 @@ def clear_recovery_restriction(*, replacement_factor=None, disable_mfa=False): if user is None: raise AuthenticationFinalizationError("account is no longer eligible") if disable_mfa: - user.mfa_enabled = False + disable_totp_mfa(user) row.assurance = AssuranceLevel.BASIC.value db.session.commit() log_security_event( diff --git a/app/totp_routes.py b/app/totp_routes.py index 7035321..240ed81 100644 --- a/app/totp_routes.py +++ b/app/totp_routes.py @@ -24,6 +24,7 @@ TOTPEnrollmentError, activate_totp_enrollment, begin_totp_enrollment, + disable_totp_mfa, verify_totp, ) from .step_up import ( @@ -228,7 +229,7 @@ def disable_mfa(): if grant_error is not None: return grant_error user = db.session.get(User, current_user.id) - user.mfa_enabled = False + disable_totp_mfa(user) db.session.commit() log_security_event("MFA_DISABLED", user=user.username) return jsonify({"ok": True}) diff --git a/app/totp_service.py b/app/totp_service.py index 7c5aae8..48f33a2 100644 --- a/app/totp_service.py +++ b/app/totp_service.py @@ -31,6 +31,14 @@ class TOTPEnrollmentError(RuntimeError): """The enrollment token, binding, or first code was invalid.""" +def disable_totp_mfa(user): + """Remove every TOTP factor and disable MFA without committing.""" + TOTPAuthenticator.query.filter_by( + user_id=user.id, + ).delete(synchronize_session=False) + user.mfa_enabled = False + + @dataclass(frozen=True) class EnrollmentView: token: str diff --git a/static/js/i18n.js b/static/js/i18n.js index 9d281aa..78e5195 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -467,6 +467,8 @@ const translations = { 'security.methodTotp': 'Authenticator app', 'security.confirmSecurityAction': 'Confirm security action', 'security.confirmDisableMfa': 'Disable every MFA factor?', + 'security.confirmDisableMfaAndRemoveTotp': 'Disable MFA and remove all authenticator apps? This cannot be undone.', + 'security.disableMfaAndRemoveTotp': 'Disable MFA and remove authenticators', 'security.invalidTotpCode': 'Enter a valid six-digit authenticator code.', 'security.mfaDisabled': 'MFA disabled', 'security.mfaEnabled': 'MFA enabled', @@ -1457,6 +1459,8 @@ const translations = { 'security.methodTotp': 'Ứng dụng xác thực', 'security.confirmSecurityAction': 'Xác nhận thao tác bảo mật', 'security.confirmDisableMfa': 'Tắt tất cả các yếu tố MFA?', + 'security.confirmDisableMfaAndRemoveTotp': 'T\u1eaft MFA v\u00e0 x\u00f3a t\u1ea5t c\u1ea3 \u1ee9ng d\u1ee5ng x\u00e1c th\u1ef1c? Kh\u00f4ng th\u1ec3 ho\u00e0n t\u00e1c thao t\u00e1c n\u00e0y.', + 'security.disableMfaAndRemoveTotp': 'T\u1eaft MFA v\u00e0 x\u00f3a tr\u00ecnh x\u00e1c th\u1ef1c', 'security.invalidTotpCode': 'Nhập mã xác thực gồm sáu chữ số hợp lệ.', 'security.mfaDisabled': 'Đã tắt MFA', 'security.mfaEnabled': 'Đã bật MFA', @@ -2520,6 +2524,8 @@ const translations = { 'security.methodTotp': 'Authenticator-App', 'security.confirmSecurityAction': 'Sicherheitsaktion bestätigen', 'security.confirmDisableMfa': 'Alle MFA-Faktoren deaktivieren?', + 'security.confirmDisableMfaAndRemoveTotp': 'MFA deaktivieren und alle Authenticator-Apps entfernen? Dies kann nicht r\u00fcckg\u00e4ngig gemacht werden.', + 'security.disableMfaAndRemoveTotp': 'MFA deaktivieren und Authenticator-Apps entfernen', 'security.invalidTotpCode': 'Gib einen gültigen sechsstelligen Authenticator-Code ein.', 'security.mfaDisabled': 'MFA deaktiviert', 'security.mfaEnabled': 'MFA aktiviert', @@ -3508,6 +3514,8 @@ const translations = { 'security.methodTotp': "Application d’authentification", 'security.confirmSecurityAction': 'Confirmer l’action de sécurité', 'security.confirmDisableMfa': 'Désactiver tous les facteurs MFA ?', + 'security.confirmDisableMfaAndRemoveTotp': 'D\u00e9sactiver l\u2019AMF et supprimer toutes les applications d\u2019authentification\u00a0? Cette action est irr\u00e9versible.', + 'security.disableMfaAndRemoveTotp': 'D\u00e9sactiver l\u2019AMF et supprimer les authentificateurs', 'security.invalidTotpCode': 'Saisissez un code d’authentification valide à six chiffres.', 'security.mfaDisabled': 'MFA désactivée', 'security.mfaEnabled': 'MFA activée', @@ -4533,6 +4541,8 @@ const translations = { 'security.methodTotp': 'Aplicación de autenticación', 'security.confirmSecurityAction': 'Confirmar acción de seguridad', 'security.confirmDisableMfa': '¿Desactivar todos los factores MFA?', + 'security.confirmDisableMfaAndRemoveTotp': '\u00bfDesactivar MFA y eliminar todas las aplicaciones de autenticaci\u00f3n? Esta acci\u00f3n no se puede deshacer.', + 'security.disableMfaAndRemoveTotp': 'Desactivar MFA y eliminar autenticadores', 'security.invalidTotpCode': 'Introduce un código de autenticación válido de seis dígitos.', 'security.mfaDisabled': 'MFA desactivado', 'security.mfaEnabled': 'MFA activado', @@ -5558,6 +5568,8 @@ const translations = { 'security.methodTotp': '身份验证器应用', 'security.confirmSecurityAction': '确认安全操作', 'security.confirmDisableMfa': '停用所有 MFA 因素?', + 'security.confirmDisableMfaAndRemoveTotp': '\u7981\u7528 MFA \u5e76\u79fb\u9664\u6240\u6709\u8eab\u4efd\u9a8c\u8bc1\u5668\u5e94\u7528\uff1f\u6b64\u64cd\u4f5c\u65e0\u6cd5\u64a4\u9500\u3002', + 'security.disableMfaAndRemoveTotp': '\u7981\u7528 MFA \u5e76\u79fb\u9664\u8eab\u4efd\u9a8c\u8bc1\u5668', 'security.invalidTotpCode': '请输入有效的六位身份验证器代码。', 'security.mfaDisabled': 'MFA 已停用', 'security.mfaEnabled': 'MFA 已启用', diff --git a/static/js/sftp-file-manager.js b/static/js/sftp-file-manager.js index cf45037..04003d2 100644 --- a/static/js/sftp-file-manager.js +++ b/static/js/sftp-file-manager.js @@ -151,7 +151,7 @@ class SFTPFileManager { left: this.createEmptyPaneState(), right: this.createEmptyPaneState(), }; - this.panes = this.workspaceEmptyPanes; + this.panes = { ...this.workspaceEmptyPanes }; this.standalonePanes = this.panes; this.embeddedPanes = null; this.activePane = 'left'; diff --git a/static/js/webauthn.js b/static/js/webauthn.js index 5de2d73..377e829 100644 --- a/static/js/webauthn.js +++ b/static/js/webauthn.js @@ -496,8 +496,8 @@ async function disableTotpMfa() { if (!window.confirm(t( - 'security.confirmDisableMfa', - 'Disable the MFA requirement for future sign-ins? Enrolled factors remain stored.' + 'security.confirmDisableMfaAndRemoveTotp', + 'Disable MFA and remove all authenticator apps? This cannot be undone.' ))) { return; } const headers = await stepUpHeaders('mfa.disable'); if (headers === null) { return; } diff --git a/templates/admin.html b/templates/admin.html index 615249d..d375960 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -328,7 +328,7 @@