diff --git a/lib/sorcery/controller.rb b/lib/sorcery/controller.rb index eb2ca099..8b3010d1 100644 --- a/lib/sorcery/controller.rb +++ b/lib/sorcery/controller.rb @@ -30,8 +30,16 @@ def require_login # Runs hooks after login or failed login. def login(*credentials) @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) + + yield(user, failure_reason) if block_given? + + return + end + old_session = session.dup.to_hash reset_sorcery_session old_session.each_pair do |k, v| @@ -41,10 +49,8 @@ def login(*credentials) auto_login(user) after_login!(user, credentials) - current_user - else - after_failed_login!(credentials) - nil + + block_given? ? yield(current_user, nil) : current_user end end diff --git a/lib/sorcery/model.rb b/lib/sorcery/model.rb index 50b670af..c36fb2fd 100644 --- a/lib/sorcery/model.rb +++ b/lib/sorcery/model.rb @@ -80,10 +80,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(return_value: false, failure: :invalid_login, &block) + end if @sorcery_config.downcase_username_before_authenticating credentials[0].downcase! @@ -91,13 +93,29 @@ def authenticate(*credentials) user = sorcery_adapter.find_by_credentials(credentials) - if user.respond_to?(:active_for_authentication?) - return nil unless 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(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 + + authentication_response(user: user, return_value: user, &block) end # encrypt tokens using current encryption_provider. @@ -112,6 +130,12 @@ def encrypt(*tokens) protected + def authentication_response(options = {}) + yield(options[:user], options[:failure]) if block_given? + + options[: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 diff --git a/lib/sorcery/model/submodules/brute_force_protection.rb b/lib/sorcery/model/submodules/brute_force_protection.rb index 3830d0eb..51950e38 100644 --- a/lib/sorcery/model/submodules/brute_force_protection.rb +++ b/lib/sorcery/model/submodules/brute_force_protection.rb @@ -41,10 +41,15 @@ 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) + return if token.blank? + + load_from_token( + token, + sorcery_config.unlock_token_attribute_name, + &block + ) end protected @@ -116,7 +121,10 @@ def prevent_locked_user_login if !login_unlocked? && config.login_lock_time_period != 0 login_unlock! if send(config.lock_expires_at_attribute_name) <= Time.now.in_time_zone end - login_unlocked? + + return false, :locked unless login_unlocked? + + true end end end diff --git a/lib/sorcery/model/submodules/reset_password.rb b/lib/sorcery/model/submodules/reset_password.rb index 45cc04f7..0948a63f 100644 --- a/lib/sorcery/model/submodules/reset_password.rb +++ b/lib/sorcery/model/submodules/reset_password.rb @@ -55,10 +55,13 @@ 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) - 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) + def load_from_reset_password_token(token, &block) + load_from_token( + token, + @sorcery_config.reset_password_token_attribute_name, + @sorcery_config.reset_password_token_expires_at_attribute_name, + &block + ) end protected diff --git a/lib/sorcery/model/submodules/user_activation.rb b/lib/sorcery/model/submodules/user_activation.rb index d8ef4fff..1c58bbbb 100644 --- a/lib/sorcery/model/submodules/user_activation.rb +++ b/lib/sorcery/model/submodules/user_activation.rb @@ -59,10 +59,13 @@ 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) - 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) + def load_from_activation_token(token, &block) + load_from_token( + token, + @sorcery_config.activation_token_attribute_name, + @sorcery_config.activation_token_expires_at_attribute_name, + &block + ) end protected @@ -128,7 +131,14 @@ def send_activation_needed_email? def prevent_non_active_login config = sorcery_config - config.prevent_non_active_users_to_login ? send(config.activation_state_attribute_name) == 'active' : true + + 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 diff --git a/lib/sorcery/model/temporary_token.rb b/lib/sorcery/model/temporary_token.rb index 99b0fdfa..e3c0c8dd 100644 --- a/lib/sorcery/model/temporary_token.rb +++ b/lib/sorcery/model/temporary_token.rb @@ -16,13 +16,34 @@ 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? + def load_from_token(token, token_attr_name, token_expiration_date_attr = nil, &block) + return token_response(failure: :invalid_token, &block) 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 + + return token_response(failure: :user_not_found, &block) unless user + + unless check_expiration_date(user, token_expiration_date_attr) + return token_response(user: user, failure: :token_expired, &block) end - user + + 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) + + !expires_at || (Time.now.in_time_zone < expires_at) + end + + def token_response(options = {}) + yield(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 e395a956..a871839d 100644 --- a/spec/controllers/controller_brute_force_protection_spec.rb +++ b/spec/controllers/controller_brute_force_protection_spec.rb @@ -20,7 +20,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 @@ -32,7 +32,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, params: { 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 1a477244..b9908764 100644 --- a/spec/controllers/controller_remember_me_spec.rb +++ b/spec/controllers/controller_remember_me_spec.rb @@ -22,7 +22,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, params: { email: 'bla@bla.com', password: 'secret' } @@ -45,7 +45,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 47a8c6ef..3abc2eda 100644 --- a/spec/controllers/controller_session_timeout_spec.rb +++ b/spec/controllers/controller_session_timeout_spec.rb @@ -39,7 +39,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, params: { email: 'bla@bla.com', password: 'secret' } @@ -50,7 +50,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, params: { 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 ea747578..a1ba1744 100644 --- a/spec/controllers/controller_spec.rb +++ b/spec/controllers/controller_spec.rb @@ -52,7 +52,7 @@ describe '#login' do 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, params: { 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 3f446f4f..f14788c7 100644 --- a/spec/shared_examples/user_activation_shared_examples.rb +++ b/spec/shared_examples/user_activation_shared_examples.rb @@ -235,6 +235,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 @@ -279,5 +300,62 @@ expect(User.load_from_activation_token(user.activation_token)).to eq user end + + describe '#load_from_activation_token' do + 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 end diff --git a/spec/shared_examples/user_reset_password_shared_examples.rb b/spec/shared_examples/user_reset_password_shared_examples.rb index 801b92f2..1f43e700 100644 --- a/spec/shared_examples/user_reset_password_shared_examples.rb +++ b/spec/shared_examples/user_reset_password_shared_examples.rb @@ -128,6 +128,71 @@ expect(User.load_from_reset_password_token('')).to be_nil end + describe '#load_from_reset_password_token' do + 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 expect(user.reset_password_token).to be_nil diff --git a/spec/shared_examples/user_shared_examples.rb b/spec/shared_examples/user_shared_examples.rb index 7ba81611..c563cc95 100644 --- a/spec/shared_examples/user_shared_examples.rb +++ b/spec/shared_examples/user_shared_examples.rb @@ -146,6 +146,31 @@ expect(User.authenticate(user.email, 'secret')).to be_nil end 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 end specify { expect(User).to respond_to(:encrypt) }