lib/simple_token_authentication/token_authentication_handler.rb
require 'active_support/concern'
require 'devise'
require 'simple_token_authentication/entities_manager'
require 'simple_token_authentication/devise_fallback_handler'
require 'simple_token_authentication/exception_fallback_handler'
require 'simple_token_authentication/sign_in_handler'
require 'simple_token_authentication/token_comparator'
module SimpleTokenAuthentication
module TokenAuthenticationHandler
extend ::ActiveSupport::Concern
included do
private_class_method :define_token_authentication_helpers_for
private_class_method :set_token_authentication_hooks
private_class_method :entities_manager
private_class_method :fallback_handler
private :authenticate_entity_from_token!
private :fallback!
private :token_correct?
private :perform_sign_in!
private :token_comparator
private :sign_in_handler
private :find_record_from_identifier
private :integrate_with_devise_case_insensitive_keys
end
def authenticate_entity_from_token!(entity)
record = find_record_from_identifier(entity)
if token_correct?(record, entity, token_comparator)
perform_sign_in!(record, sign_in_handler)
after_successful_token_authentication if respond_to?(:after_successful_token_authentication, true)
end
end
def fallback!(entity, fallback_handler)
fallback_handler.fallback!(self, entity)
end
def token_correct?(record, entity, token_comparator)
record && token_comparator.compare(record.authentication_token,
entity.get_token_from_params_or_headers(self))
end
def perform_sign_in!(record, sign_in_handler)
# Notice the store option defaults to false, so the record
# identifier is not actually stored in the session and a token
# is needed for every request. That behaviour can be configured
# through the sign_in_token option.
sign_in_handler.sign_in self, record, store: SimpleTokenAuthentication.sign_in_token
end
def find_record_from_identifier(entity)
identifier_param_value = entity.get_identifier_from_params_or_headers(self).presence
identifier_param_value = integrate_with_devise_case_insensitive_keys(identifier_param_value, entity)
# The finder method should be compatible with all the model adapters,
# namely ActiveRecord and Mongoid in all their supported versions.
identifier_param_value && entity.model.find_for_authentication(entity.identifier => identifier_param_value)
end
# Private: Take benefit from Devise case-insensitive keys
#
# See https://github.com/plataformatec/devise/blob/v3.4.1/lib/generators/templates/devise.rb#L45-L48
#
# identifier_value - the original identifier_value String
#
# Returns an identifier String value which case follows the Devise case-insensitive keys policy
def integrate_with_devise_case_insensitive_keys(identifier_value, entity)
identifier_value.downcase! if identifier_value && Devise.case_insensitive_keys.include?(entity.identifier)
identifier_value
end
def token_comparator
TokenComparator.instance
end
def sign_in_handler
SignInHandler.instance
end
module ClassMethods
# Provide token authentication handling for a token authenticatable class
#
# model - the token authenticatable Class
#
# Returns nothing.
def handle_token_authentication_for(model, options = {})
model_alias = options[:as] || options['as']
entity = entities_manager.find_or_create_entity(model, model_alias)
options = SimpleTokenAuthentication.parse_options(options)
define_token_authentication_helpers_for(entity, fallback_handler(options))
set_token_authentication_hooks(entity, options)
end
# Private: Get one (always the same) object which behaves as an entities manager
def entities_manager
if class_variable_defined?(:@@entities_manager)
class_variable_get(:@@entities_manager)
else
class_variable_set(:@@entities_manager, EntitiesManager.new)
end
end
# Private: Get one (always the same) object which behaves as a fallback authentication handler
def fallback_handler(options)
if class_variable_defined?(:@@fallback_authentication_handler)
class_variable_get(:@@fallback_authentication_handler)
else
if options[:fallback] == :exception
class_variable_set(:@@fallback_authentication_handler, ExceptionFallbackHandler.instance)
else
class_variable_set(:@@fallback_authentication_handler, DeviseFallbackHandler.instance)
end
end
end
def define_token_authentication_helpers_for(entity, fallback_handler)
method_name = "authenticate_#{entity.name_underscore}_from_token"
method_name_bang = method_name + '!'
class_eval do
define_method method_name.to_sym do
lambda { |_entity| authenticate_entity_from_token!(_entity) }.call(entity)
end
define_method method_name_bang.to_sym do
lambda do |_entity|
authenticate_entity_from_token!(_entity)
fallback!(_entity, fallback_handler)
end.call(entity)
end
end
end
def set_token_authentication_hooks(entity, options)
authenticate_method = unless options[:fallback] == :none
:"authenticate_#{entity.name_underscore}_from_token!"
else
:"authenticate_#{entity.name_underscore}_from_token"
end
if respond_to?(:before_action)
# See https://github.com/rails/rails/commit/9d62e04838f01f5589fa50b0baa480d60c815e2c
before_action authenticate_method, options.slice(:only, :except, :if, :unless)
else
before_filter authenticate_method, options.slice(:only, :except, :if, :unless)
end
end
end
end
end