From 0deba5a811cbb9b40d5f43233c7704e48a9d46f1 Mon Sep 17 00:00:00 2001
From: Jens Kraemer <jk@jkraemer.net>
Date: Mon, 24 Aug 2026 17:06:45 +0800
Subject: [PATCH] Revert "Removes ignored and unnecessary scope in twofa token
 lookup (#44074)."

Reverts commit c9b00461a6841d2ee257658705bf704093a42c91 and adds a
test asserting correct behavior.
---
 lib/redmine/twofa/base.rb           |  2 +-
 test/unit/lib/redmine/twofa_test.rb | 35 +++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 test/unit/lib/redmine/twofa_test.rb

diff --git a/lib/redmine/twofa/base.rb b/lib/redmine/twofa/base.rb
index ac943ca2c..b22531341 100644
--- a/lib/redmine/twofa/base.rb
+++ b/lib/redmine/twofa/base.rb
@@ -112,7 +112,7 @@ module Redmine
         code = code.to_s.remove(/[[:space:]]/).downcase
         user_from_code = Token.find_active_user('twofa_backup_code', code)
         # invalidate backup code after usage
-        Token.find_token('twofa_backup_code', code).try(:delete)
+        Token.where(user_id: @user.id).find_token('twofa_backup_code', code).try(:delete)
         # make sure the user using the backup code is the same it's been issued to
         return false unless @user.present? && @user == user_from_code
 
diff --git a/test/unit/lib/redmine/twofa_test.rb b/test/unit/lib/redmine/twofa_test.rb
new file mode 100644
index 000000000..292a58dad
--- /dev/null
+++ b/test/unit/lib/redmine/twofa_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require File.expand_path('../../../../test_helper', __FILE__)
+
+class Redmine::TwofaTest < ActiveSupport::TestCase
+  fixtures :users, :email_addresses
+
+  def setup
+    @user = User.find_by_login 'jsmith'
+    @other_user = User.find_by_login 'dlopper'
+  end
+
+  test "verify_backup_code! accepts and invalidates the user's own backup code" do
+    token = backup_code_for @user, 'abcdef123456'
+
+    assert Redmine::Twofa::Totp.new(@user).verify_backup_code!('abcdef123456')
+    assert_nil Token.find_by_id(token.id)
+  end
+
+  test "verify_backup_code! rejects but keeps another user's backup code" do
+    token = backup_code_for @other_user, 'abcdef654321'
+
+    assert_not Redmine::Twofa::Totp.new(@user).verify_backup_code!('abcdef654321')
+    assert Token.find_by_id(token.id), "backup code of another user must not be deleted"
+  end
+
+  private
+
+  # creates a backup code token the same way Redmine::Twofa::Base#init_backup_codes! does
+  def backup_code_for(user, value)
+    token = Token.create!(user_id: user.id, action: 'twofa_backup_code')
+    token.update_columns value: value
+    token
+  end
+end
-- 
2.55.0

