diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 4f61375d650f8..5f344bc7e45f1 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,17 @@ +* Fix `authenticate_with_http_basic` to allow for missing password. + + Before Rails 7.0 it was possible to handle basic authentication with only a username. + + ```ruby + authenticate_with_http_basic do |token, _| + ApiClient.authenticate(token) + end + ``` + + This ability is restored. + + *Jean Boussier* + * Fix `content_security_policy` returning invalid directives. Directives such as `self`, `unsafe-eval` and few others were not diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 5882096836f8b..7b984567ce1c4 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -74,6 +74,8 @@ module ClassMethods # # See ActionController::HttpAuthentication::Basic for example usage. def http_basic_authenticate_with(name:, password:, realm: nil, **options) + raise ArgumentError, "Expected name: to be a String, got #{name.class}" unless name.is_a?(String) + raise ArgumentError, "Expected password: to be a String, got #{password.class}" unless name.is_a?(String) before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm } end end @@ -82,8 +84,8 @@ def http_basic_authenticate_or_request_with(name:, password:, realm: nil, messag authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password| # This comparison uses & so that it doesn't short circuit and # uses `secure_compare` so that length information isn't leaked. - ActiveSupport::SecurityUtils.secure_compare(given_name, name) & - ActiveSupport::SecurityUtils.secure_compare(given_password, password) + ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) & + ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password) end end @@ -107,7 +109,7 @@ def authenticate(request, &login_procedure) end def has_basic_credentials?(request) - request.authorization.present? && (auth_scheme(request).downcase == "basic") && user_name_and_password(request).length == 2 + request.authorization.present? && (auth_scheme(request).downcase == "basic") end def user_name_and_password(request) diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 8c81f42a76ab6..6494b04b89296 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -31,6 +31,13 @@ def search render plain: "All inline" end + def no_password + username, password = authenticate_with_http_basic do |username, password| + [username, password] + end + render plain: "Hello #{username} (password: #{password.inspect})" + end + private def authenticate authenticate_or_request_with_http_basic do |username, password| @@ -112,11 +119,6 @@ def test_encode_credentials_has_no_newline assert_no_match(/\n/, result) end - test "has_basic_credentials? should fail with credentials without colon" do - @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("David Goliath")}" - assert_not ActionController::HttpAuthentication::Basic.has_basic_credentials?(@request) - end - test "successful authentication with uppercase authorization scheme" do @request.env["HTTP_AUTHORIZATION"] = "BASIC #{::Base64.encode64("lifo:world")}" get :index @@ -142,6 +144,21 @@ def test_encode_credentials_has_no_newline assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"] end + test "authentication request with a missing password" do + @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("David")}" + get :search + + assert_response :unauthorized + end + + test "authentication request with no required password" do + @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("George")}" + get :no_password + + assert_response :success + assert_equal "Hello George (password: nil)", @response.body + end + test "authentication request with valid credential" do @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "please") get :display