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
Changes from 1 commit
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
Next Next commit
Resurrect block authentication
Signed-off-by: Steven Hoffman <git@fustrate.com>
Fustrate committed Jan 22, 2017
commit 67b119508d14b13d96244d4b5e1ed7594120123d
18 changes: 12 additions & 6 deletions lib/sorcery/controller.rb
Original file line number Diff line number Diff line change
@@ -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

34 changes: 29 additions & 5 deletions lib/sorcery/model.rb
Original file line number Diff line number Diff line change
@@ -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.
@@ -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
18 changes: 13 additions & 5 deletions lib/sorcery/model/submodules/brute_force_protection.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 7 additions & 4 deletions lib/sorcery/model/submodules/reset_password.rb
Original file line number Diff line number Diff line change
@@ -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
20 changes: 15 additions & 5 deletions lib/sorcery/model/submodules/user_activation.rb
Original file line number Diff line number Diff line change
@@ -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_login
unless send(config.activation_state_attribute_name) == 'active'
return false, :inactive
end
end

true
end
end
end
31 changes: 26 additions & 5 deletions lib/sorcery/model/temporary_token.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions spec/controllers/controller_brute_force_protection_spec.rb
Original file line number Diff line number Diff line change
@@ -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' }
4 changes: 2 additions & 2 deletions spec/controllers/controller_remember_me_spec.rb
Original file line number Diff line number Diff line change
@@ -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

4 changes: 2 additions & 2 deletions spec/controllers/controller_session_timeout_spec.rb
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion spec/controllers/controller_spec.rb
Original file line number Diff line number Diff line change
@@ -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

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

25 changes: 25 additions & 0 deletions spec/shared_examples/user_shared_examples.rb
Original file line number Diff line number Diff line change
@@ -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) }