diff --git a/lib/sorcery/controller.rb b/lib/sorcery/controller.rb index 7479fd08..d3ff6600 100644 --- a/lib/sorcery/controller.rb +++ b/lib/sorcery/controller.rb @@ -28,23 +28,31 @@ def require_login # Takes credentials and returns a user on successful authentication. # Runs hooks after login or failed login. - def login(*credentials) + def login(*credentials, &block) @current_user = nil - user = user_class.authenticate(*credentials) - if user + + user_class.authenticate(*credentials) do |user, failure_reason| + if failure_reason + after_failed_login!(credentials) + + block.call(user, failure_reason) if block_given? + + return nil + end + old_session = session.dup.to_hash reset_sorcery_session - old_session.each_pair do |k,v| + + old_session.each_pair do |k, v| session[k.to_sym] = v end + form_authenticity_token auto_login(user) after_login!(user, credentials) - current_user - else - after_failed_login!(credentials) - nil + + block_given? ? block.call(current_user, nil) : current_user end end diff --git a/lib/sorcery/model.rb b/lib/sorcery/model.rb index b4b90095..d122d000 100644 --- a/lib/sorcery/model.rb +++ b/lib/sorcery/model.rb @@ -81,10 +81,12 @@ def sorcery_config # 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) + def authenticate(*credentials, &block) raise ArgumentError, "at least 2 arguments required" if credentials.size < 2 - return false if credentials[0].blank? + if credentials[0].blank? + return authentication_response(failure: :invalid_login, return_value: false, &block) + end if @sorcery_config.downcase_username_before_authenticating credentials[0].downcase! @@ -92,13 +94,29 @@ def authenticate(*credentials) user = sorcery_adapter.find_by_credentials(credentials) - if user.respond_to?(:active_for_authentication?) - return nil if !user.active_for_authentication? + unless user + return authentication_response(failure: :invalid_login, &block) end set_encryption_attributes - user if user && @sorcery_config.before_authenticate.all? {|c| user.send(c)} && user.valid_password?(credentials[1]) + unless user.valid_password?(credentials[1]) + return authentication_response(user: user, failure: :invalid_password, &block) + end + + if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication? + return authentication_response(failure: :inactive, user: user, &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 + + authentication_response(user: user, return_value: user, &block) end # encrypt tokens using current encryption_provider. @@ -113,11 +131,23 @@ def encrypt(*tokens) protected + def authentication_response(options = {}, &block) + block.call(options[:user], options[:failure]) if block_given? + + options[:return_value] + end + + def authentication_failed(reason, return_value = nil, user = nil, &block) + block.call(user, reason) if block_given? + + return return_value + end + 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 end - + def add_config_inheritance self.class_eval do def self.inherited(subclass) @@ -148,7 +178,7 @@ def external? # Calls the configured encryption provider to compare the supplied password with the encrypted one. def valid_password?(pass) - _crypted = self.send(sorcery_config.crypted_password_attribute_name) + _crypted = self.send(sorcery_config.crypted_password_attribute_name) return _crypted == pass if sorcery_config.encryption_provider.nil? _salt = self.send(sorcery_config.salt_attribute_name) unless sorcery_config.salt_attribute_name.nil? diff --git a/lib/sorcery/model/submodules/brute_force_protection.rb b/lib/sorcery/model/submodules/brute_force_protection.rb index e176c471..d9dd699b 100644 --- a/lib/sorcery/model/submodules/brute_force_protection.rb +++ b/lib/sorcery/model/submodules/brute_force_protection.rb @@ -41,10 +41,11 @@ def self.included(base) end module ClassMethods - def load_from_unlock_token(token) - return nil if token.blank? - user = sorcery_adapter.find_by_token(sorcery_config.unlock_token_attribute_name,token) - user + # This doesn't check to see if the account is still locked. + def load_from_unlock_token(token, &block) + load_from_token(token, + sorcery_config.unlock_token_attribute_name, + &block) end protected @@ -116,7 +117,10 @@ def prevent_locked_user_login if !self.unlocked? && config.login_lock_time_period != 0 self.unlock! if self.send(config.lock_expires_at_attribute_name) <= Time.now.in_time_zone end - unlocked? + + return false, :locked unless unlocked? + + true end end end diff --git a/lib/sorcery/model/submodules/reset_password.rb b/lib/sorcery/model/submodules/reset_password.rb index 00e8de48..3f9b2857 100644 --- a/lib/sorcery/model/submodules/reset_password.rb +++ b/lib/sorcery/model/submodules/reset_password.rb @@ -59,10 +59,10 @@ def self.included(base) module ClassMethods # Find user by token, also checks for expiration. # Returns the user if token found and is valid. - def load_from_reset_password_token(token) + def load_from_reset_password_token(token, &block) token_attr_name = @sorcery_config.reset_password_token_attribute_name token_expiration_date_attr = @sorcery_config.reset_password_token_expires_at_attribute_name - load_from_token(token, token_attr_name, token_expiration_date_attr) + load_from_token(token, token_attr_name, token_expiration_date_attr, &block) end protected diff --git a/lib/sorcery/model/submodules/user_activation.rb b/lib/sorcery/model/submodules/user_activation.rb index 5c5bd5fa..ba4db5d5 100644 --- a/lib/sorcery/model/submodules/user_activation.rb +++ b/lib/sorcery/model/submodules/user_activation.rb @@ -70,10 +70,10 @@ def self.included(base) module ClassMethods # Find user by token, also checks for expiration. # Returns the user if token found and is valid. - def load_from_activation_token(token) + def load_from_activation_token(token, &block) token_attr_name = @sorcery_config.activation_token_attribute_name token_expiration_date_attr = @sorcery_config.activation_token_expires_at_attribute_name - load_from_token(token, token_attr_name, token_expiration_date_attr) + load_from_token(token, token_attr_name, token_expiration_date_attr, &block) end protected @@ -141,9 +141,15 @@ def send_activation_needed_email? def prevent_non_active_login config = sorcery_config - config.prevent_non_active_users_to_login ? self.send(config.activation_state_attribute_name) == "active" : true - end + if config.prevent_non_active_users_to_login + unless send(config.activation_state_attribute_name) == 'active' + return false, :inactive + end + end + + true + end end end end diff --git a/lib/sorcery/model/temporary_token.rb b/lib/sorcery/model/temporary_token.rb index 44854ffd..af4b1d81 100644 --- a/lib/sorcery/model/temporary_token.rb +++ b/lib/sorcery/model/temporary_token.rb @@ -16,13 +16,44 @@ def self.generate_random_token end module ClassMethods - def load_from_token(token, token_attr_name, token_expiration_date_attr) - return nil if token.blank? - user = sorcery_adapter.find_by_token(token_attr_name,token) - if !user.blank? && !user.send(token_expiration_date_attr).nil? - return Time.now.in_time_zone < user.send(token_expiration_date_attr) ? user : nil + def load_from_token(token, token_attr_name, token_expiration_date_attr = nil, &block) + if token.blank? + return token_response(failure: :invalid_token, &block) end - user + + user = sorcery_adapter.find_by_token(token_attr_name, token) + + unless user + return token_response(failure: :user_not_found, &block) + end + + # if !user.blank? && !user.send(token_expiration_date_attr).nil? + # return Time.now.in_time_zone < user.send(token_expiration_date_attr) ? user : nil + # end + + unless check_expiration_date(user, token_expiration_date_attr) + return token_response(user: user, failure: :token_expired, return_value: nil, &block) + end + + token_response(user: user, return_value: user, &block) + end + + protected + + def check_expiration_date(user, token_expiration_date_attr) + return true unless token_expiration_date_attr + + expires_at = user.send(token_expiration_date_attr) + + return true unless expires_at + + Time.now.in_time_zone < expires_at + end + + def token_response(options = {}, &block) + block.call(options[:user], options[:failure]) if block_given? + + options[:return_value] end end end diff --git a/spec/controllers/controller_brute_force_protection_spec.rb b/spec/controllers/controller_brute_force_protection_spec.rb index cc71ccc9..f035add1 100644 --- a/spec/controllers/controller_brute_force_protection_spec.rb +++ b/spec/controllers/controller_brute_force_protection_spec.rb @@ -22,7 +22,7 @@ def request_test_login end it "counts login retries" do - allow(User).to receive(:authenticate) + allow(User).to receive(:authenticate) { |&block| block.call(nil, :other) } allow(User.sorcery_adapter).to receive(:find_by_credentials).with(['bla@bla.com', 'blabla']).and_return(user) expect(user).to receive(:register_failed_login!).exactly(3).times @@ -34,7 +34,7 @@ def request_test_login # dirty hack for rails 4 allow(@controller).to receive(:register_last_activity_time_to_db) - allow(User).to receive(:authenticate).and_return(user) + allow(User).to receive(:authenticate) { |&block| block.call(user, nil) } expect(user).to receive_message_chain(:sorcery_adapter, :update_attribute).with(:failed_logins_count, 0) get :test_login, email: 'bla@bla.com', password: 'secret' diff --git a/spec/controllers/controller_remember_me_spec.rb b/spec/controllers/controller_remember_me_spec.rb index ba262328..17dd3e6e 100644 --- a/spec/controllers/controller_remember_me_spec.rb +++ b/spec/controllers/controller_remember_me_spec.rb @@ -24,7 +24,7 @@ end it "sets cookie on remember_me!" do - expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) + expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) } expect(user).to receive(:remember_me!) post :test_login_with_remember, :email => 'bla@bla.com', :password => 'secret' @@ -47,7 +47,7 @@ end it "login(email,password,remember_me) logs user in and remembers" do - expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1').and_return(user) + expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1') { |&block| block.call(user, nil) } expect(user).to receive(:remember_me!) expect(user).to receive(:remember_me_token).and_return('abracadabra').twice diff --git a/spec/controllers/controller_session_timeout_spec.rb b/spec/controllers/controller_session_timeout_spec.rb index b445bba9..b3078805 100644 --- a/spec/controllers/controller_session_timeout_spec.rb +++ b/spec/controllers/controller_session_timeout_spec.rb @@ -40,7 +40,7 @@ it "works if the session is stored as a string or a Time" do session[:login_time] = Time.now.to_s # TODO: ??? - expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) + expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) } get :test_login, :email => 'bla@bla.com', :password => 'secret' @@ -51,7 +51,7 @@ context "with 'session_timeout_from_last_action'" do it "does not logout if there was activity" do sorcery_controller_property_set(:session_timeout_from_last_action, true) - expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) + expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) } get :test_login, :email => 'bla@bla.com', :password => 'secret' Timecop.travel(Time.now.in_time_zone+0.3) diff --git a/spec/controllers/controller_spec.rb b/spec/controllers/controller_spec.rb index a71e89a4..35a072f1 100644 --- a/spec/controllers/controller_spec.rb +++ b/spec/controllers/controller_spec.rb @@ -54,7 +54,7 @@ context "when succeeds" do before do - expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user) + expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) } get :test_login, :email => 'bla@bla.com', :password => 'secret' end diff --git a/spec/shared_examples/user_activation_shared_examples.rb b/spec/shared_examples/user_activation_shared_examples.rb index 6eb82ced..c7e2678f 100644 --- a/spec/shared_examples/user_activation_shared_examples.rb +++ b/spec/shared_examples/user_activation_shared_examples.rb @@ -237,6 +237,27 @@ expect(User.authenticate user.email, 'secret').to be_truthy end + + context 'in block mode' do + it "does not allow a non-active user to authenticate" do + sorcery_model_property_set(:prevent_non_active_users_to_login, true) + + User.authenticate(user.email, 'secret') do |user2, failure| + expect(user2).to eq user + expect(user2.activation_state).to eq 'pending' + expect(failure).to eq :inactive + end + end + + it "allows a non-active user to authenticate if configured so" do + sorcery_model_property_set(:prevent_non_active_users_to_login, false) + + User.authenticate(user.email, 'secret') do |user2, failure| + expect(user2).to eq user + expect(failure).to be_nil + end + end + end end describe "load_from_activation_token" do @@ -248,21 +269,21 @@ Timecop.return end - it "load_from_activation_token returns user when token is found" do + it "returns user when token is found" do expect(User.load_from_activation_token user.activation_token).to eq user end - it "load_from_activation_token does NOT return user when token is NOT found" do + it "does NOT return user when token is NOT found" do expect(User.load_from_activation_token "a").to be_nil end - it "load_from_activation_token returas user when token is found and not expired" do + it "returns user when token is found and not expired" do sorcery_model_property_set(:activation_token_expiration_period, 500) expect(User.load_from_activation_token user.activation_token).to eq user end - it "load_from_activation_token does NOT return user when token is found and expired" do + it "does NOT return user when token is found and expired" do sorcery_model_property_set(:activation_token_expiration_period, 0.1) user @@ -271,15 +292,70 @@ expect(User.load_from_activation_token user.activation_token).to be_nil end - it "load_from_activation_token returns nil if token is blank" do + it "returns nil if token is blank" do expect(User.load_from_activation_token nil).to be_nil expect(User.load_from_activation_token "").to be_nil end - it "load_from_activation_token is always valid if expiration period is nil" do + it "is always valid if expiration period is nil" do sorcery_model_property_set(:activation_token_expiration_period, nil) expect(User.load_from_activation_token user.activation_token).to eq user end + + context 'in block mode' do + it 'yields user when token is found' do + User.load_from_activation_token(user.activation_token) do |user2, failure| + expect(user2).to eq user + expect(failure).to be_nil + end + end + + it 'does NOT yield user when token is NOT found' do + User.load_from_activation_token('a') do |user2, failure| + expect(user2).to be_nil + expect(failure).to eq :user_not_found + end + end + + it 'yields user when token is found and not expired' do + sorcery_model_property_set(:activation_token_expiration_period, 500) + + User.load_from_activation_token(user.activation_token) do |user2, failure| + expect(user2).to eq user + expect(failure).to be_nil + end + end + + it 'yields the user and failure reason when token is found and expired' do + sorcery_model_property_set(:activation_token_expiration_period, 0.1) + user + + Timecop.travel(Time.now.in_time_zone + 0.5) + + User.load_from_activation_token(user.activation_token) do |user2, failure| + expect(user2).to eq user + expect(failure).to eq :token_expired + end + end + + it 'yields a failure reason if token is blank' do + [nil, ''].each do |token| + User.load_from_activation_token(token) do |user2, failure| + expect(user2).to be_nil + expect(failure).to eq :invalid_token + end + end + end + + it 'is always valid if expiration period is nil' do + sorcery_model_property_set(:activation_token_expiration_period, nil) + + User.load_from_activation_token(user.activation_token) do |user2, failure| + expect(user2).to eq user + expect(failure).to be_nil + end + end + end end end diff --git a/spec/shared_examples/user_reset_password_shared_examples.rb b/spec/shared_examples/user_reset_password_shared_examples.rb index 02a2b786..07e32605 100644 --- a/spec/shared_examples/user_reset_password_shared_examples.rb +++ b/spec/shared_examples/user_reset_password_shared_examples.rb @@ -90,46 +90,111 @@ Timecop.return end - it "load_from_reset_password_token returns user when token is found" do - user.generate_reset_password_token! - updated_user = User.sorcery_adapter.find(user.id) + describe 'load_from_reset_password_token' do + it "returns user when token is found" do + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) - expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user - end + expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user + end - it "load_from_reset_password_token does NOT return user when token is NOT found" do - user.generate_reset_password_token! + it "does NOT return user when token is NOT found" do + user.generate_reset_password_token! - expect(User.load_from_reset_password_token "a").to be_nil - end + expect(User.load_from_reset_password_token "a").to be_nil + end - it "load_from_reset_password_token returns user when token is found and not expired" do - sorcery_model_property_set(:reset_password_expiration_period, 500) - user.generate_reset_password_token! - updated_user = User.sorcery_adapter.find(user.id) + it "returns user when token is found and not expired" do + sorcery_model_property_set(:reset_password_expiration_period, 500) + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) - expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user - end + expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user + end + + it "does NOT return user when token is found and expired" do + sorcery_model_property_set(:reset_password_expiration_period, 0.1) + user.generate_reset_password_token! + Timecop.travel(Time.now.in_time_zone+0.5) - it "load_from_reset_password_token does NOT return user when token is found and expired" do - sorcery_model_property_set(:reset_password_expiration_period, 0.1) - user.generate_reset_password_token! - Timecop.travel(Time.now.in_time_zone+0.5) + expect(User.load_from_reset_password_token user.reset_password_token).to be_nil + end - expect(User.load_from_reset_password_token user.reset_password_token).to be_nil - end + it "is always valid if expiration period is nil" do + sorcery_model_property_set(:reset_password_expiration_period, nil) + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) - it "load_from_reset_password_token is always valid if expiration period is nil" do - sorcery_model_property_set(:reset_password_expiration_period, nil) - user.generate_reset_password_token! - updated_user = User.sorcery_adapter.find(user.id) + expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user + end - expect(User.load_from_reset_password_token user.reset_password_token).to eq updated_user - end + it "returns nil if token is blank" do + expect(User.load_from_reset_password_token nil).to be_nil + expect(User.load_from_reset_password_token "").to be_nil + end - it "load_from_reset_password_token returns nil if token is blank" do - expect(User.load_from_reset_password_token nil).to be_nil - expect(User.load_from_reset_password_token "").to be_nil + context 'in block mode' do + it "yields user when token is found" do + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) + + User.load_from_reset_password_token(user.reset_password_token) do |user2, failure| + expect(user2).to eq updated_user + expect(failure).to be_nil + end + end + + it "does NOT yield user when token is NOT found" do + user.generate_reset_password_token! + + User.load_from_reset_password_token("a") do |user2, failure| + expect(user2).to be_nil + expect(failure).to eq :user_not_found + end + end + + it "yields user when token is found and not expired" do + sorcery_model_property_set(:reset_password_expiration_period, 500) + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) + + User.load_from_reset_password_token(user.reset_password_token) do |user2, failure| + expect(user2).to eq updated_user + expect(failure).to be_nil + end + end + + it "yields user and failure reason when token is found and expired" do + sorcery_model_property_set(:reset_password_expiration_period, 0.1) + user.generate_reset_password_token! + Timecop.travel(Time.now.in_time_zone+0.5) + + User.load_from_reset_password_token(user.reset_password_token) do |user2, failure| + expect(user2).to eq user + expect(failure).to eq :token_expired + end + end + + it "is always valid if expiration period is nil" do + sorcery_model_property_set(:reset_password_expiration_period, nil) + user.generate_reset_password_token! + updated_user = User.sorcery_adapter.find(user.id) + + User.load_from_reset_password_token(user.reset_password_token) do |user2, failure| + expect(user2).to eq updated_user + expect(failure).to be_nil + end + end + + it "returns nil if token is blank" do + [nil, ''].each do |token| + User.load_from_reset_password_token(token) do |user2, failure| + expect(user2).to be_nil + expect(failure).to eq :invalid_token + end + end + end + end end it "'deliver_reset_password_instructions!' generates a reset_password_token" do diff --git a/spec/shared_examples/user_shared_examples.rb b/spec/shared_examples/user_shared_examples.rb index 242aef7d..c9fb7ac1 100644 --- a/spec/shared_examples/user_shared_examples.rb +++ b/spec/shared_examples/user_shared_examples.rb @@ -101,6 +101,31 @@ expect(User.authenticate user.email, 'wrong!').to be nil end + context 'in block mode' do + it 'yields the user if credentials are good' do + User.authenticate(user.email, 'secret') do |user2, failure| + expect(user2).to eq user + expect(failure).to be_nil + end + end + + it 'yields the user and proper error if credentials are bad' do + User.authenticate(user.email, 'wrong!') do |user2, failure| + expect(user2).to eq user + expect(failure).to eq :invalid_password + end + end + + it 'yields the proper error if no user exists' do + [nil, '', 'not@a.user'].each do |email| + User.authenticate(email, 'wrong!') do |user2, failure| + expect(user2).to be_nil + expect(failure).to eq :invalid_login + end + end + end + end + context "downcasing username" do after do sorcery_reload! @@ -262,11 +287,11 @@ class Admin2 < User; end let(:user_with_pass) { create_new_user({:username => 'foo_bar', :email => "foo@bar.com", :password => 'foobar'})} specify { expect(user_with_pass).to respond_to :valid_password? } - + it "returns true if password is correct" do expect(user_with_pass.valid_password?("foobar")).to be true end - + it "returns false if password is incorrect" do expect(user_with_pass.valid_password?("foobug")).to be false end