diff --git a/CHANGELOG.md b/CHANGELOG.md
index a720072c..4ef39d2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -97,6 +97,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added
- [#684](https://github.com/binarylogic/authlogic/pull/684) - Use cookies
only when available. Support for `ActionController::API`
+ - [#728](https://github.com/binarylogic/authlogic/pull/728) - Allow
+ single_access_token to be supplied using Headers in addition to Params.
- Fixed
- [#725](https://github.com/binarylogic/authlogic/pull/725) - `NoMethodError`
when setting `sign_cookie` or `encrypt_cookie` before `controller` is
diff --git a/lib/authlogic/session/base.rb b/lib/authlogic/session/base.rb
index 716b658a..2ba59f86 100644
--- a/lib/authlogic/session/base.rb
+++ b/lib/authlogic/session/base.rb
@@ -236,6 +236,20 @@ def initialize(session)
#
# You can modify all of this behavior with the Config sub module.
#
+ # Headers
+ # =======
+ #
+ # This module is responsible for authenticating the user via headers, which requires
+ # setting HTTP header, for example, the following curl command:
+ #
+ # curl -H "user_credentials: 4LiXF7FiGUppIPubBPey" https://www.domain.com
+ #
+ # Notice the token in the header parameter, this is a single access token. This header's
+ # method operates exactly as the params method.
+ #
+ # You can modify all of this behavior with the Config sub module.
+ #
+ # Disabled by default. To enable, set a non-nil headers_key.
# Perishable Token
# ================
#
@@ -410,6 +424,7 @@ def self.#{method}(*filter_list, &block)
# `persist` callbacks, in order of priority
persist :persist_by_params
+ persist :persist_by_headers
persist :persist_by_cookie
persist :persist_by_session
persist :persist_by_http_auth, if: :persist_by_http_auth?
@@ -857,6 +872,24 @@ def params_key(value = nil)
end
alias params_key= params_key
+ # Works exactly like cookie_key, but for headers. So a user can login via
+ # headers just like a cookie or a session. Your URL would look like:
+ #
+ # curl -H "user_credentials: 4LiXF7FiGUppIPubBPey" https://www.domain.com
+ #
+ # You can change the "user_credentials" key above with this
+ # configuration option. Keep in mind, just like cookie_key, if you
+ # supply an id the id will be appended to the front. Check out
+ # cookie_key for more details. Also checkout the "Single Access /
+ # Private Feeds Access" section in the README.
+ #
+ # * Default: nil
+ # * Accepts: String
+ def headers_key(value = nil)
+ rw_config(:headers_key, value)
+ end
+ alias headers_key= headers_key
+
# Works exactly like login_field, but for the password instead. Returns
# :password if a login_field exists.
#
@@ -1876,21 +1909,33 @@ def params_credentials
controller.params[params_key]
end
+ def headers_credentials
+ # Setting headers_key to nil is the accepted way to disable
+ # single_access_token in headers.
+ return nil if headers_key.nil?
+ controller.request.headers[headers_key]
+ end
+
def params_enabled?
- if !params_credentials || !klass.column_names.include?("single_access_token")
- return false
- end
- if controller.responds_to_single_access_allowed?
- return controller.single_access_allowed?
- end
- params_enabled_by_allowed_request_types?
+ params_credentials && single_access_token_enabled?
+ end
+
+ def headers_enabled?
+ headers_credentials && single_access_token_enabled?
+ end
+
+ def single_access_token_enabled?
+ return false unless klass.column_names.include?("single_access_token")
+ return controller.single_access_allowed? if controller.responds_to_single_access_allowed?
+
+ single_access_token_allowed_by_request_type?
end
- def params_enabled_by_allowed_request_types?
- case single_access_allowed_request_types
- when Array
- single_access_allowed_request_types.include?(controller.request_content_type) ||
- single_access_allowed_request_types.include?(:all)
+ def single_access_token_allowed_by_request_type?
+ if single_access_allowed_request_types.is_a?(Array)
+ Set.new(single_access_allowed_request_types).intersect?(
+ Set[controller.request_content_type, :all]
+ )
else
%i[all any].include?(single_access_allowed_request_types)
end
@@ -1900,6 +1945,16 @@ def params_key
build_key(self.class.params_key)
end
+ def headers_key
+ # Rack servers uppercase header names, convert hyphens to underscores,
+ # and prefix with "HTTP_" to comply with CGI. We transform the key in
+ # the same way Rails does:
+ # https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/http/headers.rb#L123-L126
+ key = build_key(self.class.headers_key)
+ return nil if key.nil?
+ "HTTP_" + key.upcase.tr("-", "_")
+ end
+
def password_field
self.class.password_field
end
@@ -1919,10 +1974,17 @@ def persist_by_cookie
end
def persist_by_params
- return false unless params_enabled?
+ persist_by_single_access_token(params_credentials) if params_enabled?
+ end
+
+ def persist_by_headers
+ persist_by_single_access_token(headers_credentials) if headers_enabled?
+ end
+
+ def persist_by_single_access_token(credentials)
self.unauthorized_record = search_for_record(
"find_by_single_access_token",
- params_credentials
+ credentials
)
self.single_access = valid?
end
diff --git a/lib/authlogic/test_case/mock_api_controller.rb b/lib/authlogic/test_case/mock_api_controller.rb
index 3fe4e38e..852c50cb 100644
--- a/lib/authlogic/test_case/mock_api_controller.rb
+++ b/lib/authlogic/test_case/mock_api_controller.rb
@@ -26,6 +26,10 @@ def params
@params ||= {}
end
+ def headers
+ @headers ||= {}
+ end
+
def request
@request ||= MockRequest.new(self)
end
diff --git a/lib/authlogic/test_case/mock_request.rb b/lib/authlogic/test_case/mock_request.rb
index 6848d17f..1d8b6b14 100644
--- a/lib/authlogic/test_case/mock_request.rb
+++ b/lib/authlogic/test_case/mock_request.rb
@@ -19,6 +19,10 @@ def format
controller.request_content_type if controller.respond_to? :request_content_type
end
+ def headers
+ @headers ||= {}
+ end
+
def ip
controller&.respond_to?(:env) &&
controller.env.is_a?(Hash) &&
diff --git a/test/session_test/params_test.rb b/test/session_test/single_access_token_test.rb
similarity index 71%
rename from test/session_test/params_test.rb
rename to test/session_test/single_access_token_test.rb
index 5d280f13..6a7843b5 100644
--- a/test/session_test/params_test.rb
+++ b/test/session_test/single_access_token_test.rb
@@ -3,9 +3,11 @@
require "test_helper"
module SessionTest
- module ParamsTest
+ module SingleAccessTokenTest
class ConfigTest < ActiveSupport::TestCase
def test_params_key
+ assert_equal UserSession.cookie_key, UserSession.params_key
+
UserSession.params_key = "my_params_key"
assert_equal "my_params_key", UserSession.params_key
@@ -13,6 +15,16 @@ def test_params_key
assert_equal "user_credentials", UserSession.params_key
end
+ def test_headers_key
+ assert_equal nil, UserSession.headers_key
+
+ UserSession.headers_key = "my_headers_key"
+ assert_equal "my_headers_key", UserSession.headers_key
+
+ UserSession.headers_key "user_credentials"
+ assert_equal "user_credentials", UserSession.headers_key
+ end
+
def test_single_access_allowed_request_types
UserSession.single_access_allowed_request_types = ["my request type"]
assert_equal ["my request type"], UserSession.single_access_allowed_request_types
@@ -28,11 +40,21 @@ def test_single_access_allowed_request_types
class InstanceMethodsTest < ActiveSupport::TestCase
def test_persist_persist_by_params
+ assert_persist_by(:params)
+ end
+
+ def test_persist_persist_by_headers
+ # Since default headers_key is nil, set for the test.
+ UserSession.send("headers_key=", "user_credentials")
+ assert_persist_by(:headers)
+ end
+
+ def assert_persist_by(headers_or_params)
ben = users(:ben)
session = UserSession.new
refute session.persisting?
- set_params_for(ben)
+ send("set_#{headers_or_params}_for", ben)
refute session.persisting?
refute session.unauthorized_record
diff --git a/test/test_helper.rb b/test/test_helper.rb
index f2e3de8c..d55d36c2 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -274,6 +274,14 @@ def unset_params
controller.params["user_credentials"] = nil
end
+ def set_headers_for(user)
+ controller.request.headers["HTTP_USER_CREDENTIALS"] = user.single_access_token
+ end
+
+ def unset_headers
+ controller.request.headers["HTTP_USER_CREDENTIALS"] = nil
+ end
+
def set_request_content_type(type)
controller.request_content_type = type
end