stonean

trying to be better than I was yesterday

Lockdown with Authlogic

I created a very quick project on github that represents these instructions:

Rails Cornerstone

If you are starting a new project, I’d recommend cloning this project. Also, you can help me help others by improving on this project and making it easier for everyone to have a secured application.

Now on to the instructions…

Authlogic has very good readme with great details. A lot of this information comes from that page, so if you would like to know more about authlogic, it’s all there for you.

These instructions include the password reset functionality.

If this is in any way incomplete, please let me know.

First you’ll need the gems:


  stonean$ sudo gem install authlogic lockdown

Then you’ll need to add the config.gem statement for authlogic:

Important: *Only use the config.gem statement for lockdown with :lib => false.
Lockdown is required via config/initializers/lockit.rb

Note: By the time you read this, the version numbers may have changed

 
  config.gem 'authlogic', :version => '2.0.13'
  config.gem 'lockdown', :lib => false, :version => '1.0.0'

Let’s now add your user_session model:

You won’t need to make any changes after to this model after running the generator

stonean$ ./script/generate session user_session

Let’s now add your user model:

We will make some changes after adding in Lockdown.

stonean$ ./script/generate model user

I modified my migration:

You can find out more about the options on the authlogic readme

 
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  # required
      t.string    :perishable_token,    :null => false 
      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 indexes
    #
    add_index :users, :perishable_token
    add_index :users, :email    
    #
    # Create your initial user.  
    # Will make this an administrator user later.
    #
    usr = User.create \
              :login => 'admin',
              :email => 'admin@yoursite.com',
              :password => 'betterpassword',
              :password_confirmation => 'betterpassword'
  end
  #
  #
  def self.down
    drop_table :users
  end
end

Routes


  map.resource :account, :controller => 'users'
  map.resources :users
  map.resources :password_resets
  map.resource :user_session
  map.login '/login', :controller => 'user_sessions', :action => 'new'
  map.logout '/logout', :controller => 'user_sessions', :action => 'destroy'

Controllers

Here is what your application_controller will look like:

Note :logged_in? and :current_user_is_admin? are methods provided by Lockdown


class ApplicationController < ActionController::Base
  helper_method \
    :current_user_session, 
    :current_user, 
    :logged_in?, 
    :current_user_is_admin?

  filter_parameter_logging :password, :password_confirmation

  protect_from_forgery

  protected

  def clear_authlogic_session
    sess = current_user_session
    sess.destroy if sess
  end

  private

  def current_user
    if defined?(@current_user) && !@current_user.nil?
      return @current_user 
    end
    @current_user = current_user_session && current_user_session.user
  end

  def current_user_session
    if defined?(@current_user_session) && !@current_user_session.nil?
      return @current_user_session 
    end
    @current_user_session = UserSession.find
  end
 
end

Here is what your users_controller will look like:


class UsersController < ApplicationController
  def index
    @users = User.find(:all)
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    @user.save do |result|
      if result
        flash[:notice] = "Account registered!"
        add_lockdown_session_values
        if params[:action_todo]
          redirect_to params[:action_todo]
        else
          redirect_back_or_default(account_url)
        end
      else
        render :action => :new
      end
    end
  end

  def show
    @user = current_user
  end

  def edit
    @user = current_user
  end

  def update
    @user = current_user # makes our views "cleaner" and more consistent
    if @user.update_attributes(params[:user])
      flash[:notice] = "Account updated!"
      redirect_to account_url
    else
      render :action => :edit
    end
  end

  def destroy
    if current_user_is_admin?
      user = User.find(params[:id])
      user.destroy
      flash[:notice] = "User #{user.login} deleted!"
    end

    redirect_to root_path
  end
end

Here is what your user_sessions_controller will look like:

Note: add_lockdown_session_values(user) is required by Lockdown to initialize the session


class UserSessionsController < ApplicationController
  after_filter :set_lockdown_values, :only => :create

  def new
    @user_session = UserSession.new
  end

  def create
    @user_session = UserSession.new(params[:user_session])
    @user_session.save do |result|
      if result
        flash[:notice] = "Login successful!"
        redirect_back_or_default root_path
      else
        render :action => :new
      end
    end
  end

  def destroy
    current_user_session.destroy
    reset_lockdown_session
    flash[:notice] = "Logout successful!"
    redirect_to root_path
  end

  private

  def set_lockdown_values
    if user = @user_session.user
      add_lockdown_session_values(user)
    end
  end
end

Here is what your password_resets_controller will look like:


class PasswordResetsController < ApplicationController
  before_filter :load_user_using_perishable_token, :only => [:edit, :update]

  def new
    render
  end

  def create
    @user = User.find_by_email(params[:email])
    if @user
      @user.deliver_password_reset_instructions!
      flash[:notice] = "Instructions to reset your password" +
        "have been emailed to you.\n" +
        "Please check your email."
      redirect_to root_url
    else
      flash[:notice] = "No user was found with that email address"
      render :action => :new
    end
  end

  def edit
    render
  end

  def update
    @user.password = params[:user][:password]
    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.save
      flash[:notice] = "Password successfully updated"
      redirect_to account_url
    else
      render :action => :edit
    end
  end

  private

  def load_user_using_perishable_token
    @user = User.find_using_perishable_token(params[:id])
    unless @user
      flash[:notice] = "Sorry, but we could not locate your account. " +
        "If you are having issues try copying and pasting the URL " +
        "from your email into your browser or restarting the " +
        "reset password process."
      redirect_to root_url
    end
  end
end

For the password resets you’ll need a mailer. I like to put these in RAILS_ROOT/app/mailers.
If you do that, add the following to your config/environment.rb:


config.load_paths += %W( #{RAILS_ROOT}/app/mailers )

Here’s what your app/mailers/notifier.rb will look like:

Note: modify the mycool stuff with something cooler (and correct)


class Notifier < ActionMailer::Base
  default_url_options[:host] = "mycoolsite.com"

  def password_reset_instructions(user)
    subject      "Password Reset Instructions"
    from          "MyCoolSite Notifier<noreply@mycooldomain.com>"
    recipients    user.email
    sent_on       Time.now
    body          :edit_password_reset_url => \
                    edit_password_reset_url(user.perishable_token)
  end

end

Outside the scope of this, but you’ll of course need to setup ActionMailer.

I use gmail, if you want to use gmail you’ll need the smtp_tls gem.

Added the following to the end of your config/environment.rb:


require 'smtp_tls'

ActionMailer::Base.delivery_method = :smtp

ActionMailer::Base.smtp_settings = {
  :address => 'smtp.gmail.com',
  :port => 587,
  :user_name => 'user@mycooldomain.com',
  :password => 'my_password',
  :authentication => :plain,
}

Lockdown


stonean$ ./script/generate lockdown
      exists  app/views
      exists  app/controllers
      exists  app/helpers
      create  lib/lockdown
      create  lib/lockdown/README
      create  lib/lockdown/init.rb
      exists  app/models
      create  app/models/user_group.rb
      create  app/models/permission.rb
      exists  db/migrate
      create  db/migrate/004_create_user_groups.rb
      exists  db/migrate
      create  db/migrate/005_create_permissions.rb
      create  config/initializers/lockit.rb

Don’t run rake db:migrate yet

Don’t forget to remove RAILS_ROOT/public/index.html

Modify your create_user_groups migration:
bq. Note: Adding: Lockdown::System.make_user_administrator(User.find(1)) to make your initial user an administrator


class CreateUserGroups < ActiveRecord::Migration
  def self.up
    create_table :user_groups do |t|
      t.string :name

      t.timestamps
    end

    create_table :user_groups_users, :id => false do |t|
      t.integer :user_group_id
      t.integer :user_id
    end

    # This will create the 'Administrator' user group and 
    # associate it to the user.
    Lockdown::System.make_user_administrator(User.find(1))
  end

  def self.down
    drop_table :user_groups_users
    drop_table :user_groups
  end
end

Modify your user model:

Note: has_and_belongs_to_many :user_groups is required for Lockdown


class User < ActiveRecord::Base
  has_and_belongs_to_many :user_groups

  acts_as_authentic 

  def deliver_password_reset_instructions!
    reset_perishable_token!
    Notifier.deliver_password_reset_instructions(self)
  end
end

Now run:


stonean$ rake db:migrate --trace

You won’t need to modify your user_group or permission models.

Views

PasswordResets views (app/views/password_resets).

new.html.haml:


%h1 Forgot Password

%p Fill out the form below and instructions to reset your password will be emailed to you:

- form_tag password_resets_path do
%p
%label{:for => ’email’} Email
= text_field_tag :email
= submit_tag ‘Reset my password’

edit.html.haml:


%h1 Change My Password

- form_for @user, :url => password_reset_path, :method => :put do |f|
= f.error_messages
%p
%label{:for => "user_password"} Password
= f.password_field :password
%p
%label{:for => "user_password_confirmation"} Confirm Password
= f.password_field :password_confirmation

= f.submit “Update my password and log me in”

UserSession views (app/views/user_sessions).

new.html.haml:


%h1 Login
- form_for @user_session, :url => user_session_path do |f|
  = f.error_messages
  %p
    %label{:for => "user_session_login"} Login:
    = f.text_field :login
  %p
    %label{:for => "user_session_password"} Password:
    = f.password_field :password
  %p.checkbox
    = f.check_box :remember_me
    %label{:for => "user_session_remember_me"} Remember Me
  %br
  = f.submit "Login"
%br
  = link_to 'Forgot your password?', :controller => 'password_resets', :action => 'new'

User views (app/views/users).

new.html.haml:


%h1 Register

- form_for @user, :url => account_path do |f|
  = f.error_messages
  = render :partial => "form", :object => f
  = f.submit "Register"

edit.html.haml:


%h1 Edit My Account

- form_for @user, :url => account_path do |f|
  = f.error_messages
  = render :partial => "form", :object => f
  = f.submit "Update"

= link_to "My Profile", account_path

_form.haml:


%p
  %label{:for => "user_email"} Email
  = form.text_field :email
%p
  %label{:for => "user_login"} Login
  = form.text_field :login
%p
  %label{:for => "user_password"} Password
  = form.password_field :password
%p
  %label{:for => "user_password_confirmation"} Confirm Password
  = form.password_field :password_confirmation

show.html.haml:


%p
  %span.strong Email:
  = @user.email
%p
  %span.strong Login:
  = @user.login

= link_to 'Edit', edit_account_path

Grant Access!

Before you’ll be able to see anything you’ll need to grant access.

Modify your lib/lockdown/init.rb and define your permissions:


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Public Access
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
  set_permission(:login).with_controller(:user_sessions)

  set_permission(:register_account).
    with_controller(:users).
    only_methods(:new, :create).
    and_controller(:password_resets)
  

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Protected Access
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  set_permission(:my_account).
    with_controller(:users).
    only_methods(:show, :edit, :update)

Now assign these new permissions to the built-in public_access user group.

Modify your lib/lockdown/init.rb and set the previous permissions as public:


  # Define the built-in user groups here:
  #
  # Available to the whole world
  set_public_access :login, :register_account
  #
  # Must be logged_in to access these:
  set_protected_access :my_account

You’ll also need to sync the authlogic session with the lockdown session. To do this just set the following option in lib/lockdown/init.rb:

 options[:session_timeout_method] = :clear_authlogic_session 

We added the clear_authlogic_session method to the application controller earlier

Please Note:

Lockdown checks for access on each request. If you don’t have access defined in init.rb, access will not be allowed.
When changing public access make sure you clear your session. Login/Logout is the easiest way, but if you haven’t defined those actions you may need to clear your session (cookie, database record, etc…)
Defining/redefining public access trips people up more than anything else.

Copyright © 2010 stonean. All rights reserved.
Powered by Thoth.