Lockdown with Authlogic
I created a very quick project on github that represents these instructions:
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.
