Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resurrect block authentication #41

Merged
merged 2 commits into from
Mar 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions lib/sorcery/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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

Expand Down
34 changes: 29 additions & 5 deletions lib/sorcery/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,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(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)

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.
Expand All @@ -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
Expand Down
18 changes: 13 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,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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions lib/sorcery/model/submodules/reset_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions lib/sorcery/model/submodules/user_activation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 26 additions & 5 deletions lib/sorcery/model/temporary_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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(['[email protected]', 'blabla']).and_return(user)

expect(user).to receive(:register_failed_login!).exactly(3).times
Expand All @@ -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: '[email protected]', 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 @@ -22,7 +22,7 @@
end

it 'sets cookie on remember_me!' do
expect(User).to receive(:authenticate).with('[email protected]', 'secret').and_return(user)
expect(User).to receive(:authenticate).with('[email protected]', 'secret') { |&block| block.call(user, nil) }
expect(user).to receive(:remember_me!)

post :test_login_with_remember, params: { email: '[email protected]', password: 'secret' }
Expand All @@ -45,7 +45,7 @@
end

it 'login(email,password,remember_me) logs user in and remembers' do
expect(User).to receive(:authenticate).with('[email protected]', 'secret', '1').and_return(user)
expect(User).to receive(:authenticate).with('[email protected]', '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 @@ -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('[email protected]', 'secret').and_return(user)
expect(User).to receive(:authenticate).with('[email protected]', 'secret') { |&block| block.call(user, nil) }

get :test_login, params: { email: '[email protected]', password: 'secret' }

Expand All @@ -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('[email protected]', 'secret').and_return(user)
expect(User).to receive(:authenticate).with('[email protected]', 'secret') { |&block| block.call(user, nil) }

get :test_login, params: { email: '[email protected]', 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 @@ -52,7 +52,7 @@
describe '#login' do
context 'when succeeds' do
before do
expect(User).to receive(:authenticate).with('[email protected]', 'secret').and_return(user)
expect(User).to receive(:authenticate).with('[email protected]', 'secret') { |&block| block.call(user, nil) }
get :test_login, params: { email: '[email protected]', password: 'secret' }
end

Expand Down
78 changes: 78 additions & 0 deletions spec/shared_examples/user_activation_shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading