Skip to content
24 changes: 16 additions & 8 deletions lib/sorcery/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 37 additions & 7 deletions lib/sorcery/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,42 @@ 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!
end

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.
Expand All @@ -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)
Expand Down Expand Up @@ -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?
Expand Down
14 changes: 9 additions & 5 deletions lib/sorcery/model/submodules/brute_force_protection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/sorcery/model/submodules/reset_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions lib/sorcery/model/submodules/user_activation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 37 additions & 6 deletions lib/sorcery/model/temporary_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/controllers/controller_brute_force_protection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions spec/controllers/controller_remember_me_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions spec/controllers/controller_session_timeout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion spec/controllers/controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading