Blog
what did i learn today
authlogic on rails3

If you want to get authlogic working in a fresh rails 3 project, it will take a bit more steps than devise. This has everything to do with vision: authlogic only claims to deliver you the backend, allowing itself to remain more stable and to easily replace other authentication libraries (even your home-brewn). That seems a great philosophy, but starting from scratch involves more steps. First you need to setup a clean rails3 project, as i described before.

Install the gem

As simple as adding the following line to your Gemfile : [ruby] gem "authlogic" gem "rails3-generators" [/ruby] (note: you need the rails3-generators to include the needed generators). Then run bundle install or bundle update.

Create the UserSession

[ruby] rails g authlogic:session UserSession [/ruby] This would be all, but in my installation it was not enough. Something inside rails3 broke the authlogic session. But the fix, luckily, is pretty easy: you have to add the to_key function. So your complete UserSession model will look as follows: [ruby] class UserSession < Authlogic::Session::Base def to_key new_record? ? nil : [self.send(self.class.primary_key)] end end [/ruby]

Create the User

If you do not yet have a User model, and i am assuming you don't, you need to [ruby] rails g model User [/ruby] For now, this is an empty model. You will need to fill in your migration to create the model correctly. [ruby wraplines="false"] class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :login, :null => false t.string :email, :null => false t.string :crypted_password, :null => false t.string :password_salt, :null => false t.string :persistence_token, :null => false #t.string :single_access_token, :null => false # optional, see Authlogic::Session::Params #t.string :perishable_token, :null => false # optional, see Authlogic::Session::Perishability # magic fields (all optional, see Authlogic::Session::MagicColumns) t.integer :login_count, :null => false, :default => 0 t.integer :failed_login_count, :null => false, :default => 0 t.datetime :last_request_at t.datetime :current_login_at t.datetime :last_login_at t.string :current_login_ip t.string :last_login_ip t.timestamps end add_index :users, ["login"], :name => "index_users_on_login", :unique => true add_index :users, ["email"], :name => "index_users_on_email", :unique => true add_index :users, ["persistence_token"], :name => "index_users_on_persistence_token", :unique => true end def self.down drop_table :users end end [/ruby] Now we still have to add some code to the User model : [ruby] class User < ActiveRecord::Base acts_as_authentic end [/ruby]

ApplicationController

We need to add some generic code to our applicationcontroller to persist the sessions, to check whether a user is required and do the correct redirects when needed. [ruby] class ApplicationController < ActionController::Base protect_from_forgery helper_method :current_user_session, :current_user private def current_user_session logger.debug "ApplicationController::current_user_session" return @current_user_session if defined?(@current_user_session) @current_user_session = UserSession.find end def current_user logger.debug "ApplicationController::current_user" return @current_user if defined?(@current_user) @current_user = current_user_session && current_user_session.user end def require_user logger.debug "ApplicationController::require_user" unless current_user store_location flash[:notice] = "You must be logged in to access this page" redirect_to new_user_session_url return false end end def require_no_user logger.debug "ApplicationController::require_no_user" if current_user store_location flash[:notice] = "You must be logged out to access this page" redirect_to account_url return false end end def store_location session[:return_to] = request.request_uri end def redirect_back_or_default(default) redirect_to(session[:return_to] || default) session[:return_to] = nil end end [/ruby]

UserSessionsController

[ruby] rails g controller UserSessions new [/ruby] and fill the controller with the correct code: [ruby] class UserSessionsController < ApplicationController before_filter :require_no_user, :only => [:new, :create] before_filter :require_user, :only => :destroy def new @user_session = UserSession.new end def create @user_session = UserSession.new(params[:user_session]) if @user_session.save flash[:notice] = "Login successful!" redirect_back_or_default users_url else render :action => :new end end def destroy current_user_session.destroy flash[:notice] = "Logout successful!" redirect_back_or_default new_user_session_url end end [/ruby] and of course that also needs a view, in new.html.haml[ruby] %h1 Login = form_for @user_session, :url => {:action => "create"} do |f| = f.error_messages %div = f.label :login = f.text_field :login %div = f.label :password = f.password_field :password %div = f.check_box :remember_me = f.label :remember_me %div = f.submit "Login" [/ruby] We want to use the f.error_message, but that is now removed from Rails3 and we need to install a plugin instead: [ruby] rails plugin install git://github.com/rails/dynamic_form.git [/ruby] We also need to define, inside config/routes.rb. You will see that the generator will have added the route get 'user_sessions/new', but that is not enough. You will have to add: [ruby] resources :user_sessions match 'login' => "user_sessions#new", :as => :login match 'logout' => "user_sessions#destroy", :as => :logout [/ruby]

Restrict access

Suppose you now have some other controller, e.g. HomeController, then restricting acces is straightforward: [ruby] rails g controller home index [/ruby] [ruby] class HomeController < ApplicationController before_filter :require_user def index end end [/ruby] The method require_user, defined in ApplicationController, will check if there is a user logged on, and if not redirect to the login-page. If we now delete the index.html inside your public folder, and we add the following at the bottom of config/routes.rb[ruby] root :to => 'home#index' [/ruby] Now we can start our application, and it should redirect to our login-page. But of course, since we have no users for now, we can't login just yet. To allow testing, you fire up your console: [bash] $ rails c Loading development environment (Rails 3.0.0.rc) ruby-1.9.2-p0 > User.create(:login => 'test', :email => 'test@tester.com', :password => 'test123', :password_confirmation => 'test123') => #<User id: 1, login: "test", email: "test@tester.com", crypted_password: "0129d9733b7912017e37a50263901488da90e127e6fd1ae6081...", password_salt: "ygufdflWkJQZGhbsGyia", persistence_token: "957788609b2067092dd3852b01613d53f96d0692ce5131174ca...", login_count: 0, failed_login_count: 0, last_request_at: nil, current_login_at: nil, last_login_at: nil, current_login_ip: nil, last_login_ip: nil, created_at: "2010-08-29 14:11:04", updated_at: "2010-08-29 14:11:04"> ruby-1.9.2-p0 > [/bash] Now you should be able to login using this user. For completeness, you should add some links to your application-view to allow logging in and out: [ruby] #user_nav - if current_user = "Signed in as #{current_user.email}. Not you?" = link_to "Sign out", logout_path - else = link_to "Sign in", new_user_session_path [/ruby] This would get your rails3 project started. The next steps would be to add some user management, or allowing users to sign up themselves, and maybe add some roles to limit certain users access if needed. Now, to contrast this with devise: i now have a bunch of code in my application that actually is not specific to my code, but is also completely not tested. I will provide example rspec tests for this later. Actually, the problem with this scenario as outlined, is it is one big bang. You should start little, declaring things you need to be able to do, and work in little steps to get there. But this scenario is intended as a draft to see how you could get there. Now, in your real application, you should start with the tests, and then from this example you now how you can implement it.If you want to get authlogic working in a fresh rails 3 project, it will take a bit more steps than devise. This has everything to do with vision: authlogic only claims to deliver you the backend, allowing itself to remain more stable and to easily replace other authentication libraries (even your home-brewn). That seems a great philosophy, but starting from scratch involves more steps. First you need to setup a clean rails3 project, as i described before.

Install the gem

As simple as adding the following line to your Gemfile : [ruby] gem "authlogic" [/ruby] and then run bundle install or bundle update.

Create the UserSession

[ruby] rails g authlogic:session UserSession [/ruby] This would be all, but in my installation it was not enough. Something inside rails3 broke the authlogic session. But the fix, luckily, is pretty easy: you have to add the to_key function. So your complete UserSession model will look as follows: [ruby] class UserSession < Authlogic::Session::Base def to_key new_record? ? nil : [self.send(self.class.primary_key)] end end [/ruby]

Create the User

If you do not yet have a User model, and i am assuming you don't, you need to [ruby] rails g model User [/ruby] For now, this is an empty model. You will need to fill in your migration to create the model correctly. [ruby wraplines="false"] class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :login, :null => false t.string :email, :null => false t.string :crypted_password, :null => false t.string :password_salt, :null => false t.string :persistence_token, :null => false #t.string :single_access_token, :null => false # optional, see Authlogic::Session::Params #t.string :perishable_token, :null => false # optional, see Authlogic::Session::Perishability # magic fields (all optional, see Authlogic::Session::MagicColumns) t.integer :login_count, :null => false, :default => 0 t.integer :failed_login_count, :null => false, :default => 0 t.datetime :last_request_at t.datetime :current_login_at t.datetime :last_login_at t.string :current_login_ip t.string :last_login_ip t.timestamps end add_index :users, ["login"], :name => "index_users_on_login", :unique => true add_index :users, ["email"], :name => "index_users_on_email", :unique => true add_index :users, ["persistence_token"], :name => "index_users_on_persistence_token", :unique => true end def self.down drop_table :users end end [/ruby] Now we still have to add some code to the User model : [ruby] class User < ActiveRecord::Base acts_as_authentic end [/ruby]

ApplicationController

We need to add some generic code to our applicationcontroller to persist the sessions, to check whether a user is required and do the correct redirects when needed. [ruby] class ApplicationController < ActionController::Base protect_from_forgery helper_method :current_user_session, :current_user private def current_user_session logger.debug "ApplicationController::current_user_session" return @current_user_session if defined?(@current_user_session) @current_user_session = UserSession.find end def current_user logger.debug "ApplicationController::current_user" return @current_user if defined?(@current_user) @current_user = current_user_session && current_user_session.user end def require_user logger.debug "ApplicationController::require_user" unless current_user store_location flash[:notice] = "You must be logged in to access this page" redirect_to new_user_session_url return false end end def require_no_user logger.debug "ApplicationController::require_no_user" if current_user store_location flash[:notice] = "You must be logged out to access this page" redirect_to account_url return false end end def store_location session[:return_to] = request.request_uri end def redirect_back_or_default(default) redirect_to(session[:return_to] || default) session[:return_to] = nil end end [/ruby]

UserSessionsController

[ruby] rails g controller UserSessions new [/ruby] and fill the controller with the correct code: [ruby] class UserSessionsController < ApplicationController before_filter :require_no_user, :only => [:new, :create] before_filter :require_user, :only => :destroy def new @user_session = UserSession.new end def create @user_session = UserSession.new(params[:user_session]) if @user_session.save flash[:notice] = "Login successful!" redirect_back_or_default users_url else render :action => :new end end def destroy current_user_session.destroy flash[:notice] = "Logout successful!" redirect_back_or_default new_user_session_url end end [/ruby] and of course that also needs a view, in new.html.haml[ruby] %h1 Login = form_for @user_session, :url => {:action => "create"} do |f| = f.error_messages %div = f.label :login = f.text_field :login %div = f.label :password = f.password_field :password %div = f.check_box :remember_me = f.label :remember_me %div = f.submit "Login" [/ruby] We want to use the f.error_message, but that is now removed from Rails3 and we need to install a plugin instead: [ruby] rails plugin install git://github.com/rails/dynamic_form.git [/ruby] We also need to define, inside config/routes.rb. You will see that the generator will have added the route get 'user_sessions/new', but that is not enough. You will have to add: [ruby] resources :user_sessions match 'login' => "user_sessions#new", :as => :login match 'logout' => "user_sessions#destroy", :as => :logout [/ruby]

Restrict access

Suppose you now have some other controller, e.g. HomeController, then restricting acces is straightforward: [ruby] rails g controller home index [/ruby] [ruby] class HomeController < ApplicationController before_filter :require_user def index end end [/ruby] The method require_user, defined in ApplicationController, will check if there is a user logged on, and if not redirect to the login-page. If we now delete the index.html inside your public folder, and we add the following at the bottom of config/routes.rb[ruby] root :to => 'home#index' [/ruby] Now we can start our application, and it should redirect to our login-page. But of course, since we have no users for now, we can't login just yet. To allow testing, you fire up your console: [bash] $ rails c Loading development environment (Rails 3.0.0.rc) ruby-1.9.2-p0 > User.create(:login => 'test', :email => 'test@tester.com', :password => 'test123', :password_confirmation => 'test123') => #<User id: 1, login: "test", email: "test@tester.com", crypted_password: "0129d9733b7912017e37a50263901488da90e127e6fd1ae6081...", password_salt: "ygufdflWkJQZGhbsGyia", persistence_token: "957788609b2067092dd3852b01613d53f96d0692ce5131174ca...", login_count: 0, failed_login_count: 0, last_request_at: nil, current_login_at: nil, last_login_at: nil, current_login_ip: nil, last_login_ip: nil, created_at: "2010-08-29 14:11:04", updated_at: "2010-08-29 14:11:04"> ruby-1.9.2-p0 > [/bash] Now you should be able to login using this user. For completeness, you should add some links to your application-view to allow logging in and out: [ruby] #user_nav - if current_user = "Signed in as #{current_user.email}. Not you?" = link_to "Sign out", logout_path - else = link_to "Sign in", new_user_session_path [/ruby] This would get your rails3 project started. The next steps would be to add some user management, or allowing users to sign up themselves, and maybe add some roles to limit certain users access if needed. Now, to contrast this with devise: i now have a bunch of code in my application that actually is not specific to my code, but is also completely not tested. I will provide example rspec tests for this later. Actually, the problem with this scenario as outlined, is it is one big bang. You should start little, declaring things you need to be able to do, and work in little steps to get there. But this scenario is intended as a draft to see how you could get there. Now, in your real application, you should start with the tests, and then from this example you now how you can implement it.