module Sorcery # This module handles all plugin operations which are related to the Model layer in the MVC pattern. # It should be included into the ORM base class. # In the case of Rails this is usually ActiveRecord (actually, in that case, the plugin does this automatically). # # When included it defines a single method: 'authenticates_with_sorcery!' # which when called adds the other capabilities to the class. # This method is also the place to configure the plugin in the Model layer. module Model def authenticates_with_sorcery! @sorcery_config = Config.new extend ClassMethods # included here, before submodules, so they can be overriden by them. include InstanceMethods include TemporaryToken include_required_submodules! # This runs the options block set in the initializer on the model class. ::Sorcery::Controller::Config.user_config.tap { |blk| blk.call(@sorcery_config) if blk } define_base_fields init_orm_hooks! @sorcery_config.after_config << :add_config_inheritance if @sorcery_config.subclasses_inherit_config @sorcery_config.after_config.each { |c| send(c) } end private def define_base_fields class_eval do sorcery_config.username_attribute_names.each do |username| sorcery_adapter.define_field username, String, length: 255 end unless sorcery_config.username_attribute_names.include?(sorcery_config.email_attribute_name) sorcery_adapter.define_field sorcery_config.email_attribute_name, String, length: 255 end sorcery_adapter.define_field sorcery_config.crypted_password_attribute_name, String, length: 255 sorcery_adapter.define_field sorcery_config.salt_attribute_name, String, length: 255 end end # includes required submodules into the model class, # which usually is called User. def include_required_submodules! class_eval do @sorcery_config.submodules = ::Sorcery::Controller::Config.submodules @sorcery_config.submodules.each do |mod| # TODO: Is there a cleaner way to handle missing submodules? # rubocop:disable Lint/HandleExceptions begin include Submodules.const_get(mod.to_s.split('_').map(&:capitalize).join) rescue NameError # don't stop on a missing submodule. Needed because some submodules are only defined # in the controller side. end # rubocop:enable Lint/HandleExceptions end end end # add virtual password accessor and ORM callbacks. def init_orm_hooks! sorcery_adapter.define_callback :before, :validation, :encrypt_password, if: proc { |record| record.send(sorcery_config.password_attribute_name).present? } sorcery_adapter.define_callback :after, :save, :clear_virtual_password, if: proc { |record| record.send(sorcery_config.password_attribute_name).present? } attr_accessor sorcery_config.password_attribute_name end module ClassMethods # Returns the class instance variable for configuration, when called by the class itself. def sorcery_config @sorcery_config end # The default authentication method. # Takes a username and password, # Finds the user by the username and compares the user's password to the one supplied to the method. # returns the user if success, nil otherwise. def authenticate(*credentials, &block) raise ArgumentError, 'at least 2 arguments required' if credentials.size < 2 if credentials[0].blank? return authentication_response(return_value: false, failure: :invalid_login, &block) end if @sorcery_config.downcase_username_before_authenticating credentials[0].downcase! end user = sorcery_adapter.find_by_credentials(credentials) unless user return authentication_response(failure: :invalid_login, &block) end set_encryption_attributes if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication? return authentication_response(user: user, failure: :inactive, &block) end @sorcery_config.before_authenticate.each do |callback| success, reason = user.send(callback) unless success return authentication_response(user: user, failure: reason, &block) end end unless user.valid_password?(credentials[1]) return authentication_response(user: user, failure: :invalid_password, &block) end authentication_response(user: user, return_value: user, &block) end # encrypt tokens using current encryption_provider. def encrypt(*tokens) return tokens.first if @sorcery_config.encryption_provider.nil? set_encryption_attributes CryptoProviders::AES256.key = @sorcery_config.encryption_key @sorcery_config.encryption_provider.encrypt(*tokens) end # FIXME: This method of passing config to the hashing provider is # questionable, and has been refactored in Sorcery v1. def set_encryption_attributes @sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches @sorcery_config.encryption_provider.join_token = @sorcery_config.salt_join_token if @sorcery_config.encryption_provider.respond_to?(:join_token) && @sorcery_config.salt_join_token @sorcery_config.encryption_provider.pepper = @sorcery_config.pepper if @sorcery_config.encryption_provider.respond_to?(:pepper) && @sorcery_config.pepper end protected def authentication_response(options = {}) yield(options[:user], options[:failure]) if block_given? options[:return_value] end def add_config_inheritance class_eval do def self.inherited(subclass) subclass.class_eval do class << self attr_accessor :sorcery_config end end subclass.sorcery_config = sorcery_config super end end end end module InstanceMethods # Returns the class instance variable for configuration, when called by an instance. def sorcery_config self.class.sorcery_config end # identifies whether this user is regular, i.e. we hold his credentials in our db, # or that he is external, and his credentials are saved elsewhere (twitter/facebook etc.). def external? send(sorcery_config.crypted_password_attribute_name).nil? end # Calls the configured encryption provider to compare the supplied password with the encrypted one. def valid_password?(pass) crypted = send(sorcery_config.crypted_password_attribute_name) return crypted == pass if sorcery_config.encryption_provider.nil? # Ensure encryption provider is using configured values self.class.set_encryption_attributes salt = send(sorcery_config.salt_attribute_name) unless sorcery_config.salt_attribute_name.nil? sorcery_config.encryption_provider.matches?(crypted, pass, salt) end protected # creates new salt and saves it. # encrypts password with salt and saves it. def encrypt_password config = sorcery_config send(:"#{config.salt_attribute_name}=", new_salt = TemporaryToken.generate_random_token) unless config.salt_attribute_name.nil? send(:"#{config.crypted_password_attribute_name}=", self.class.encrypt(send(config.password_attribute_name), new_salt)) end def clear_virtual_password config = sorcery_config send(:"#{config.password_attribute_name}=", nil) return unless respond_to?(:"#{config.password_attribute_name}_confirmation=") send(:"#{config.password_attribute_name}_confirmation=", nil) end # calls the requested email method on the configured mailer # supports both the ActionMailer 3 way of calling, and the plain old Ruby object way. def generic_send_email(method, mailer) config = sorcery_config mail = config.send(mailer).send(config.send(method), self) return unless mail.respond_to?(config.email_delivery_method) mail.send(config.email_delivery_method) end end end end