Settings → Account: the second-factor card promises an authenticator app to passkey-only accounts, and a code \"Always\" on trusted browsers #428

Closed
opened 2026-10-01 10:46:38 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the Account page against allauth's MFA stages.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at accounts/settings_views.py:192 (mfa_enabled); the template, accounts/stages.py and the settings have not moved.

The Two-factor authentication card on the Account page describes how sign-in is protected, and gets it wrong in two cases. An account whose only second factor is a passkey or security key is told it has an authenticator app. And every account with a second factor is told that a password sign-in asks for a code "Always", although a browser the person chose to trust is not asked for 30 days.

  • Where: templates/settings/account.html:150-151 and :166; accounts/settings_views.py:190, where mfa_enabled is get_mfa_adapter().is_mfa_enabled(self.request.user) with no types; accounts/stages.py:18-20, the module docstring; config/settings/base.py:281-282, MFA_TRUST_ENABLED = True and MFA_TRUST_COOKIE_AGE = timedelta(days=30).
  • What happens:
    1. allauth's is_mfa_enabled(user, types=None) is true for any Authenticator row, a WebAuthn key included. Somebody who has added only a passkey therefore reads "On. Signing in asks for a code from your authenticator app, or a recovery code." They have no authenticator app; after a password, allauth asks them for the key.
    2. The list under it says "With a password: you are asked for a code. Always." allauth's AuthenticateStage._should_handle, which SecondFactorStage calls first, returns False when trust.is_trusted_browser(request, user) is true, and the trust step is offered after every successful second factor (tests/test_mfa.py::test_a_trusted_browser_is_not_asked_again exercises exactly this). The stages.py docstring repeats the same "always".
  • Why it matters: this card exists so that nobody has to work out why they were asked for a code once and not the next time (its own comment at account.html:162-163); as written it produces that confusion, and it misstates to a person how their account is protected.
  • Fix: work out which factors exist, TOTP with is_mfa_enabled(user, [Authenticator.Type.TOTP]) and keys from passkeys.count (or a WebAuthn filter), and word the status for each case: an app, a key, or both. Reword the password line to say the second factor is asked for unless this browser was marked as trusted, for 30 days (taken from MFA_TRUST_COOKIE_AGE, not hard-coded), and correct the docstring in stages.py. New strings go through makemessages as usual.

Done when

  • A test in tests/test_mfa.py gives a person only a WebAuthn authenticator and finds that the Account page does not mention an authenticator app, and that the password line mentions a trusted browser; the same page for a TOTP-only account still names the app.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the Account page against allauth's MFA stages. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `accounts/settings_views.py:192` (`mfa_enabled`); the template, `accounts/stages.py` and the settings have not moved. The Two-factor authentication card on the Account page describes how sign-in is protected, and gets it wrong in two cases. An account whose only second factor is a passkey or security key is told it has an authenticator app. And every account with a second factor is told that a password sign-in asks for a code "Always", although a browser the person chose to trust is not asked for 30 days. - **Where:** `templates/settings/account.html:150-151` and `:166`; `accounts/settings_views.py:190`, where `mfa_enabled` is `get_mfa_adapter().is_mfa_enabled(self.request.user)` with no types; `accounts/stages.py:18-20`, the module docstring; `config/settings/base.py:281-282`, `MFA_TRUST_ENABLED = True` and `MFA_TRUST_COOKIE_AGE = timedelta(days=30)`. - **What happens:** 1. allauth's `is_mfa_enabled(user, types=None)` is true for any `Authenticator` row, a WebAuthn key included. Somebody who has added only a passkey therefore reads "On. Signing in asks for a code from your authenticator app, or a recovery code." They have no authenticator app; after a password, allauth asks them for the key. 2. The list under it says "With a password: you are asked for a code. Always." allauth's `AuthenticateStage._should_handle`, which `SecondFactorStage` calls first, returns `False` when `trust.is_trusted_browser(request, user)` is true, and the trust step is offered after every successful second factor (`tests/test_mfa.py::test_a_trusted_browser_is_not_asked_again` exercises exactly this). The `stages.py` docstring repeats the same "always". - **Why it matters:** this card exists so that nobody has to work out why they were asked for a code once and not the next time (its own comment at `account.html:162-163`); as written it produces that confusion, and it misstates to a person how their account is protected. - **Fix:** work out which factors exist, TOTP with `is_mfa_enabled(user, [Authenticator.Type.TOTP])` and keys from `passkeys.count` (or a WebAuthn filter), and word the status for each case: an app, a key, or both. Reword the password line to say the second factor is asked for unless this browser was marked as trusted, for 30 days (taken from `MFA_TRUST_COOKIE_AGE`, not hard-coded), and correct the docstring in `stages.py`. New strings go through `makemessages` as usual. ## Done when - A test in `tests/test_mfa.py` gives a person only a WebAuthn authenticator and finds that the Account page does not mention an authenticator app, and that the password line mentions a trusted browser; the same page for a TOTP-only account still names the app.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:38 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#428
No description provided.