Skip to content
Open
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 76 additions & 14 deletions lib/authlogic/session/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ================
#
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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.
#
# * <tt>Default:</tt> nil
# * <tt>Accepts:</tt> 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.
#
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/authlogic/test_case/mock_api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def params
@params ||= {}
end

def headers
@headers ||= {}
end

def request
@request ||= MockRequest.new(self)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/authlogic/test_case/mock_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@
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

UserSession.params_key "user_credentials"
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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down