# frozen_string_literal: true require 'kingsman/hooks/activatable' require 'kingsman/hooks/csrf_cleaner' module Kingsman module Models # Authenticatable module. Holds common settings for authentication. # # == Options # # Authenticatable adds the following options to +kingsman+: # # * +authentication_keys+: parameters used for authentication. By default [:email]. # # * +http_authentication_key+: map the username passed via HTTP Auth to this parameter. Defaults to # the first element in +authentication_keys+. # # * +request_keys+: parameters from the request object used for authentication. # By specifying a symbol (which should be a request method), it will automatically be # passed to find_for_authentication method and considered in your model lookup. # # For instance, if you set :request_keys to [:subdomain], :subdomain will be considered # as key on authentication. This can also be a hash where the value is a boolean specifying # if the value is required or not. # # * +http_authenticatable+: if this model allows http authentication. By default false. # It also accepts an array specifying the strategies that should allow http. # # * +params_authenticatable+: if this model allows authentication through request params. By default true. # It also accepts an array specifying the strategies that should allow params authentication. # # * +skip_session_storage+: By default Kingsman will store the user in session. # By default is set to skip_session_storage: [:http_auth]. # # == active_for_authentication? # # After authenticating a user and in each request, Kingsman checks if your model is active by # calling model.active_for_authentication?. This method is overwritten by other kingsman modules. For instance, # :confirmable overwrites .active_for_authentication? to only return true if your model was confirmed. # # You can overwrite this method yourself, but if you do, don't forget to call super: # # def active_for_authentication? # super && special_condition_is_valid? # end # # Whenever active_for_authentication? returns false, Kingsman asks the reason why your model is inactive using # the inactive_message method. You can overwrite it as well: # # def inactive_message # special_condition_is_valid? ? super : :special_condition_is_not_valid # end # module Authenticatable extend ActiveSupport::Concern UNSAFE_ATTRIBUTES_FOR_SERIALIZATION = [:encrypted_password, :reset_password_token, :reset_password_sent_at, :remember_created_at, :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip, :password_salt, :confirmation_token, :confirmed_at, :confirmation_sent_at, :remember_token, :unconfirmed_email, :failed_attempts, :unlock_token, :locked_at] included do class_attribute :kingsman_modules, instance_writer: false self.kingsman_modules ||= [] before_validation :downcase_keys before_validation :strip_whitespace end def self.required_fields(klass) [] end # Check if the current object is valid for authentication. This method and # find_for_authentication are the methods used in a Warden::Strategy to check # if a model should be signed in or not. # # However, you should not overwrite this method, you should overwrite active_for_authentication? # and inactive_message instead. def valid_for_authentication? block_given? ? yield : true end def unauthenticated_message :invalid end def active_for_authentication? true end def inactive_message :inactive end def authenticatable_salt end # Redefine serializable_hash in models for more secure defaults. # By default, it removes from the serializable model all attributes that # are *not* accessible. You can remove this default by using :force_except # and passing a new list of attributes you want to exempt. All attributes # given to :except will simply add names to exempt to Kingsman internal list. def serializable_hash(options = nil) options = options.try(:dup) || {} options[:except] = Array(options[:except]).dup if options[:force_except] options[:except].concat Array(options[:force_except]) else options[:except].concat UNSAFE_ATTRIBUTES_FOR_SERIALIZATION end super(options) end # Redefine inspect using serializable_hash, to ensure we don't accidentally # leak passwords into exceptions. def inspect inspection = serializable_hash.collect do |k,v| "#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}" end "#<#{self.class} #{inspection.join(", ")}>" end protected def kingsman_mailer Kingsman.mailer end # This is an internal method called every time Kingsman needs # to send a notification/mail. This can be overridden if you # need to customize the e-mail delivery logic. For instance, # if you are using a queue to deliver e-mails (active job, delayed # job, sidekiq, resque, etc), you must add the delivery to the queue # just after the transaction was committed. To achieve this, # you can override send_kingsman_notification to store the # deliveries until the after_commit callback is triggered. # # The following example uses Active Job's `deliver_later` : # # class User # kingsman :database_authenticatable, :confirmable # # after_commit :send_pending_kingsman_notifications # # protected # # def send_kingsman_notification(notification, *args) # # If the record is new or changed then delay the # # delivery until the after_commit callback otherwise # # send now because after_commit will not be called. # # For Rails < 6 use `changed?` instead of `saved_changes?`. # if new_record? || saved_changes? # pending_kingsman_notifications << [notification, args] # else # render_and_send_kingsman_message(notification, *args) # end # end # # private # # def send_pending_kingsman_notifications # pending_kingsman_notifications.each do |notification, args| # render_and_send_kingsman_message(notification, *args) # end # # # Empty the pending notifications array because the # # after_commit hook can be called multiple times which # # could cause multiple emails to be sent. # pending_kingsman_notifications.clear # end # # def pending_kingsman_notifications # @pending_kingsman_notifications ||= [] # end # # def render_and_send_kingsman_message(notification, *args) # message = kingsman_mailer.send(notification, self, *args) # # # Deliver later with Active Job's `deliver_later` # if message.respond_to?(:deliver_later) # message.deliver_later # # Remove once we move to Rails 4.2+ only, as `deliver` is deprecated. # elsif message.respond_to?(:deliver_now) # message.deliver_now # else # message.deliver # end # end # # end # def send_kingsman_notification(notification, *args) message = kingsman_mailer.send(notification, self, *args) # Remove once we move to Rails 4.2+ only. if message.respond_to?(:deliver_now) message.deliver_now else message.deliver end end def downcase_keys self.class.case_insensitive_keys.each { |k| apply_to_attribute_or_variable(k, :downcase) } end def strip_whitespace self.class.strip_whitespace_keys.each { |k| apply_to_attribute_or_variable(k, :strip) } end def apply_to_attribute_or_variable(attr, method) if self[attr] self[attr] = self[attr].try(method) # Use respond_to? here to avoid a regression where globally # configured strip_whitespace_keys or case_insensitive_keys were # attempting to strip or downcase when a model didn't have the # globally configured key. elsif respond_to?(attr) && respond_to?("#{attr}=") new_value = send(attr).try(method) send("#{attr}=", new_value) end end module ClassMethods Kingsman::Models.config(self, :authentication_keys, :request_keys, :strip_whitespace_keys, :case_insensitive_keys, :http_authenticatable, :params_authenticatable, :skip_session_storage, :http_authentication_key) def serialize_into_session(record) [record.to_key, record.authenticatable_salt] end def serialize_from_session(key, salt) record = to_adapter.get(key) record if record && record.authenticatable_salt == salt end def params_authenticatable?(strategy) params_authenticatable.is_a?(Array) ? params_authenticatable.include?(strategy) : params_authenticatable end def http_authenticatable?(strategy) http_authenticatable.is_a?(Array) ? http_authenticatable.include?(strategy) : http_authenticatable end # Find first record based on conditions given (ie by the sign in form). # This method is always called during an authentication process but # it may be wrapped as well. For instance, database authenticatable # provides a `find_for_database_authentication` that wraps a call to # this method. This allows you to customize both database authenticatable # or the whole authenticate stack by customize `find_for_authentication.` # # Overwrite to add customized conditions, create a join, or maybe use a # namedscope to filter records while authenticating. # Example: # # def self.find_for_authentication(tainted_conditions) # find_first_by_auth_conditions(tainted_conditions, active: true) # end # # Finally, notice that Kingsman also queries for users in other scenarios # besides authentication, for example when retrieving a user to send # an e-mail for password reset. In such cases, find_for_authentication # is not called. def find_for_authentication(tainted_conditions) find_first_by_auth_conditions(tainted_conditions) end def find_first_by_auth_conditions(tainted_conditions, opts = {}) to_adapter.find_first(kingsman_parameter_filter.filter(tainted_conditions).merge(opts)) end # Find or initialize a record setting an error if it can't be found. def find_or_initialize_with_error_by(attribute, value, error = :invalid) #:nodoc: find_or_initialize_with_errors([attribute], { attribute => value }, error) end # Find or initialize a record with group of attributes based on a list of required attributes. def find_or_initialize_with_errors(required_attributes, attributes, error = :invalid) #:nodoc: attributes.try(:permit!) attributes = attributes.to_h.with_indifferent_access .slice(*required_attributes) .delete_if { |key, value| value.blank? } if attributes.size == required_attributes.size record = find_first_by_auth_conditions(attributes) and return record end new(kingsman_parameter_filter.filter(attributes)).tap do |record| required_attributes.each do |key| record.errors.add(key, attributes[key].blank? ? :blank : error) end end end protected def kingsman_parameter_filter @kingsman_parameter_filter ||= Kingsman::ParameterFilter.new(case_insensitive_keys, strip_whitespace_keys) end end end end end