A current project requires that there be multiple models that can sign
in and each one must use the same sign in form. The original
SessionsController#create
action looked like the following:
def create
if user = (Owner.authenticate(params[:user]) || Employee.authenticate(params[:user]))
session[:user_id] = user.id
session[:user_class] = user.class
redirect_to dashboard_path
else
render :action => :new
end
end
We’re using has_secure_password
and rolling our own authentication.
Considering that, the above was good enough. But… looking down
the line for this app it is likely we will have to support authentication
for more than just two models on the same form. I also don’t like having
logic in my controllers. So I decided to break this logic out and I
chose the Strategy Pattern to help.
I like putting all of my strategies into
app/strategies
. This required me to add this directory to the Rails
autoload_paths
. Simply open up config/application.rb
(not necessary in Rails 3.1+, thanks Artur Roszczyk)
config.autoload_paths += %W(#{config.root}/app/strategies)
Next I wrote up a simple spec, thankfully I already had the logic from
the controller so there wasn’t much work to be done here. This went into
spec/strategies/authentication_strategy_spec.rb
require 'spec_helper'
describe AuthenticationStrategy do
context 'authenticating an owner' do
let(:owner) { mock('Owner') }
before do
owner.stubs(:authenticate).returns(owner)
Owner.stubs(:where).returns([owner])
end
it 'returns an owner' do
AuthenticationStrategy.run(:email => 'owner@example.com', :password => 'password').should eq owner
end
end
context 'authenticating an employee' do
let(:employee) { mock('Employee') }
before do
employee.stubs(:authenticate).returns(employee)
Employee.stubs(:where).returns([employee])
end
it 'returns an employee' do
AuthenticationStrategy.run(:email => 'employee@example.com', :password => 'password').should eq employee
end
end
describe 'failing to authenticate' do
context 'with no attributes' do
it 'returns nil' do
AuthenticationStrategy.run.should be_nil
end
end
context 'with no match for owner or employee' do
it 'returns nil' do
AuthenticationStrategy.run(:email => 'test@example.com', :password => 'password').should be_nil
end
end
end
end
Now it was time to make these specs green! The strategy file goes into
app/strategies/authentication_strategy.rb
class AuthenticationStrategy
def self.run(attributes = nil)
return nil if (attributes.nil? || attributes[:email].blank? || attributes[:password].blank?)
Owner.authenticate(attributes) || Employee.authenticate(attributes)
end
end
And finally to clean up the controller
def create
if user = AuthenticationStrategy.run(params[:user])
session[:user_id] = user.id
session[:user_class] = user.class
redirect_to dashboard_path
else
render :action => :new
end
end
In the end this may appear to be more work than is necessary. Keep in mind that app requirements will expand to support more models. The wins should be obvious considering that context. If the requirements grow to 5 or 6 models perhaps at that point it makes sense to actually break the authentication up into Identities with a polymorphic association to the different models. But we’ll cross that road when we get there.