From dcb576618b868b888a5b1b31e35491f300ce4050 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 24 Jul 2026 13:53:13 +0200 Subject: Complete the login only after the second factor Enrolled users get a pending marker instead of a session after the password step; a valid code through the challenge writes the real session via reset_session. otp_required without enrollment funnels into setup everywhere except the enrollment, user, and login machinery. --- test/controllers/admin_controller_test.rb | 7 ++++ test/controllers/otp_challenges_controller_test.rb | 39 ++++++++++++++++++++++ test/controllers/sessions_controller_test.rb | 15 +++++++++ 3 files changed, 61 insertions(+) create mode 100644 test/controllers/otp_challenges_controller_test.rb (limited to 'test') diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index cba4a59b..a177851f 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -38,4 +38,11 @@ class AdminControllerTest < ActionController::TestCase assert_equal [], json["tags"] assert_equal [], json["nodes"] end + + test "otp_required users without enrollment are funneled to setup" do + users(:quentin).update!(:otp_required => true) + login_as :quentin + get :index + assert_redirected_to edit_user_path(users(:quentin)) + end end diff --git a/test/controllers/otp_challenges_controller_test.rb b/test/controllers/otp_challenges_controller_test.rb new file mode 100644 index 00000000..470e84a2 --- /dev/null +++ b/test/controllers/otp_challenges_controller_test.rb @@ -0,0 +1,39 @@ +require "test_helper" + +class OtpChallengesControllerTest < ActionController::TestCase + fixtures :users + + def setup + @user = users(:quentin) + @user.update!(:otp_secret => ROTP::Base32.random) + @request.session[:pending_otp_user_id] = @user.id + @request.session[:otp_deadline] = 2.minutes.from_now.to_i + @request.session[:otp_attempts] = 0 + end + + test "a valid code completes the login" do + post :create, params: { :code => ROTP::TOTP.new(@user.otp_secret).now } + assert_equal @user.id, session[:user_id] + assert_nil session[:pending_otp_user_id] + end + + test "a wrong code does not log in" do + post :create, params: { :code => "000000" } + assert_nil session[:user_id] + assert_response :success + end + + test "the pending window expires" do + @request.session[:otp_deadline] = 1.minute.ago.to_i + post :create, params: { :code => ROTP::TOTP.new(@user.otp_secret).now } + assert_nil session[:user_id] + assert_redirected_to login_path + end + + test "attempts are limited" do + 5.times { post :create, params: { :code => "000000" } } + post :create, params: { :code => ROTP::TOTP.new(@user.otp_secret).now } + assert_nil session[:user_id] + assert_redirected_to login_path + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index a5f511f5..62acd28a 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -23,4 +23,19 @@ class SessionsControllerTest < ActionController::TestCase assert_nil session[:user_id] assert_response :redirect end + + test "login with password only is withheld for enrolled users" do + users(:quentin).update!(:otp_secret => ROTP::Base32.random) + post :create, params: { login: 'quentin', password: 'monkey' } + assert_nil session[:user_id] + assert_equal users(:quentin).id, session[:pending_otp_user_id] + assert_redirected_to new_otp_challenge_path + end + + test "otp_required without enrollment logs in but funnels into setup" do + users(:quentin).update!(:otp_required => true) + post :create, params: { login: 'quentin', password: 'monkey' } + assert session[:user_id] + assert_redirected_to edit_user_path(users(:quentin)) + end end -- cgit v1.3