Defect #44461
openRequired TOTP enrollment fails when background requests trigger pairing again
Description
Summary¶
When two-factor authentication is set to "required", TOTP enrollment
fails if a background request triggers check_twofa_activation after
the enrollment page has displayed its QR code.
In our installation, the triggering request is:
/people/avatar?id=<id>&size=80 (Redmine People plugin (PRO version))
The logs show that this request is redirected by
check_twofa_activation to the TOTP activation confirmation page.
Blocking /people/avatar* in the browser allowed the affected user
to reactivate TOTP successfully on the first attempt.
Users who already have TOTP activated can authenticate normally.
Environment¶
- Redmine: 6.1.4.stable
- Two-factor authentication setting: required
- Affected account: administrator, normal users
- Component providing PeopleController#avatar: redmine_people 1.6.13
- Database server version: postgres:17.9-alpine3.22
- Browser and authenticator application: Google authenticator
Output of RAILS_ENV=production bundle exec ruby bin/about:
Environment: Redmine version 6.1.4.stable Ruby version 3.4.10-p104 (2026-06-30) [x86_64-linux] Rails version 7.2.3.2 Environment production Database adapter PostgreSQL Mailer queue ActiveJob::QueueAdapters::AsyncAdapter Mailer delivery smtp Redmine settings: Redmine theme Default SCM: Subversion 1.14.5 Mercurial 7.0.1 Bazaar 3.3.11 Git 2.47.3 Filesystem Redmine plugins: additionals 4.5.0 redmine_agile 1.6.13 redmine_checklists 4.0.2 redmine_contacts 4.4.6 redmine_drive 1.2.5 redmine_issue_templates 1.2.2 redmine_messenger 1.0.15 redmine_people 1.6.13 redmine_questions 1.0.8 redmine_recurring_tasks 0.3.7 redmine_reporter 2.0.6 redmine_resources 2.0.8 redmine_saml 1.0.6 redmine_slack_lookup 1.0.0 redmine_theme_changer 0.7.1 redmine_vote 2.0.1 redmine_zenedit 3.0.0 redmineup_tags 2.1.1
Steps to reproduce in the affected installation¶
- Set Administration > Settings > Authentication > Two-factor authentication to "required".
- Use an account that needs to enroll in TOTP. We also encountered this after deactivating TOTP, logging out, and logging in again.
- Log in and allow Redmine to redirect to /my/twofa/totp/activate/confirm.
- The activation page renders, and the browser subsequently requests /people/avatar?id=<id>&size=80.
- That avatar request is intercepted by check_twofa_activation and redirected to /my/twofa/totp/activate/confirm.
- Scan the QR code displayed in the original activation page and submit the current authenticator code.
- Redmine rejects the code, preventing completion of enrollment.
Expected result¶
Unrelated background requests should not invalidate the TOTP secret
represented by the QR code already displayed to the user.
The user should be able to complete enrollment using that QR code,
while mandatory two-factor authentication remains enforced.
Actual result¶
TOTP enrollment fails with an invalid-code error.
Existing users with completed TOTP enrollment can still authenticate.
Confirmed diagnostic workaround¶
I blocked requests matching /people/avatar* using browser request
blocking and retried enrollment using the newly displayed QR code.
TOTP reactivation then succeeded on the first attempt, with
two-factor authentication still set to "required".
This confirms that the avatar request is relevant to the failure
in our installation.
Relevant request sequence¶
Abbreviated from production logs on 2026-09-16.
The hostname and avatar ID have been anonymized.
16:06:13.156579 Redirected to https://redmine.example.com/my/twofa/totp/activate/confirm 16:06:13.156665 Filter chain halted as :check_twofa_activation rendered or redirected 16:06:13.381066 Started GET "/my/twofa/totp/activate/confirm" 16:06:13.382859 Processing by TwofaController#activate_confirm as HTML 16:06:13.557271 Completed 200 OK 16:06:13.820394 Started GET "/people/avatar?id=<id>&size=80" 16:06:13.822111 Processing by PeopleController#avatar as HTML 16:06:13.837624 Redirected to https://redmine.example.com/my/twofa/totp/activate/confirm 16:06:13.837736 Filter chain halted as :check_twofa_activation rendered or redirected 16:06:14.064815 Started GET "/my/twofa/totp/activate/confirm" 16:06:14.066363 Processing by TwofaController#activate_confirm as HTML
This excerpt shows the request sequence before code submission;
it does not include the failed submission itself.
Suspected cause based on Redmine 6.1.4 source¶
ApplicationController#check_twofa_activation calls
init_twofa_pairing_and_send_code_for when enrollment is required
and only one two-factor authentication scheme is available.
That helper calls twofa.init_pairing!.
Redmine::Twofa::Totp#init_pairing! unconditionally generates and
stores a new key:
@user.update!(twofa_totp_key: ROTP::Base32.random)
The apparent sequence is therefore:
- The initial enrollment flow generates secret A.
- The activation page displays a QR code containing secret A.
- The background avatar request triggers the mandatory-enrollment filter again.
- Pairing initialization replaces the stored secret with secret B.
- The user submits a code derived from A, but verification uses B.
The stored secret was not directly captured in the supplied logs.
This explanation is based on the source code, request sequence,
and successful enrollment when the avatar request was blocked.
Relevant source:
https://github.com/redmine/redmine/blob/6.1.4/app/controllers/application_controller.rb
https://github.com/redmine/redmine/blob/6.1.4/lib/redmine/twofa/totp.rb
Suggested correction and regression coverage¶
Repeated automatic redirects into an existing enrollment flow
should not replace its pending TOTP secret.
Please also consider simultaneous requests during initialization,
so they cannot initialize different secrets for the same pending
enrollment.
Explicit deactivation/reset should retain its intended secret
rotation behavior, and mandatory 2FA enforcement should remain
unchanged.
A regression test could verify that a user can complete enrollment
with the originally displayed secret after an additional protected
request triggers check_twofa_activation.
The confirmed reproduction above involves our avatar component;
a plugin-free reproduction is not included in this report.
Potentially related issues¶
#43835 and #44217 describe similar enrollment failures, but I have
not verified that they have the same underlying cause.