Hacking Restful Authentication

June 08, 2010

As I mentioned, I’ve used Restful Authentication to add login functionality to several Rails applications over the last four years. While newer methods for authentication have come along, you may have a legacy application still using Restful Authentication, and swapping out your login system may be more trouble than a whole rewrite. However, if you want to stick with your existing code, and add a few features to extend the usefulness of Restful Authentication, read on. I’ll be making the following changes:

  • Add fields for users’ names and a boolean field to mark some users as administrators
  • Add authorization settings
  • Add methods to allow authorized users to edit user accounts

This assumes you’ve got an application up and running and are already using Restful Authentication for a login system.

First, create a migration to add the new fields to the users table (script/generate migration add_name_and_is_admin_to_users):

db/migrate/add_name_and_is_admin_to_users.rb:
  class AddNameAndAdminToUsers < ActiveRecord::Migration
    def self.up
      add_column :users, :firstname,  :string
      add_column :users, :lastname,   :string
      add_column :users, :is_admin,   :boolean
    end
    
    def self.down
      remove_column :users, :firstname
      remove_column :users, :lastname
      remove_column :users, :is_admin
    end
  end

Next up are a couple of changes required to the User model (app/models/users.rb). I’ve omitted some of the code generated by the Restful Authentication gem to focus on the changes, but the key thing to be aware of here is that using an authorization system (Declarative Authorization, CanCan, RESTful_ACL, etc.) makes it much easier to make sure only users with appropriate permissions may edit accounts. RESTful_ACL, used here, expects these settings to be made in the model, while other mechanisms have separate configuration files to store authorization rules. I’ll be covering authorization more in-depth in the next week or two, but basically what’s going on here is:

  • Admins can view an index (listing) of users
  • New accounts can be created by guests (not logged in) or admins, but not non-admins
  • An account may be edited by the account holder or an admin
  • An account may not be destroyed by anyone (just for sake of simplicity here; you could change this to allow people to delete their own accounts)
  • An account can be viewed (via a show method) by the account holder or an admin

You’ll also need to make sure any new fields you’ve included in the migration above are listed among the model’s attr_accessible values (note in the comments that in some cases you might not want to make values accessible, but that’s a topic better covered elsewhere).

app/models/user.rb:
  require 'digest/sha1'

  class User < ActiveRecord::Base
    include Authentication
    include Authentication::ByPassword
    include Authentication::ByCookieToken

    # Restful Authentication validations here. 
    # I validate firstname and lastname, too:

    validates_presence_of   :firstname, :lastname

    # don't forget to make the new fields attr_accessible. 
    # You might not want to make :is_admin accessible here--this is 
    # just for quick demonstration.
    attr_accessible :login, :email, :name, :password, :password_confirmation,
      :firstname, :lastname, :fullname, :is_admin

    # define this

    def fullname
      [self.firstname, self.lastname].join(' ')
    end

    # Authorization settings are handled by RESTful_ACL.

    def self.is_indexable_by(accessing_user, parent = nil)
      accessing_user.is_admin?
    end

    def self.is_creatable_by(user, parent = nil)
      user == nil or user.is_admin?
    end

    def is_updatable_by(accessing_user, parent = nil)
      id == accessing_user.id or accessing_user.is_admin?
    end

    def is_deletable_by(accessing_user, parent = nil)
      false
    end

    def is_readable_by(user, parent = nil)
      id.eql?(user.id) or user.is_admin?
    end

    # Restful Authentication code resumes here.

  end
  

Next up is the controller. This should be pretty straightforward–I’m using two before_filter calls to first check for login on all methods except new and create, then assuming that passes I’m using RESTful_ACL’s has_permission? to make sure the current user is allowed to access the method. The only other thing of note is in update. Since I’m giving site administrators the ability to add and edit accounts, I don’t want to log administrators in as the new account (whereas I would log in a new user with his new account).

index and show are pretty straightforward, like they’d be in any RESTful controller/view combo, so I’m not going to detail them.

app/controllers/users_controller.rb:
  class UsersController < ApplicationController

    # require authentication for index, show, edit, update, destroy
    before_filter :login_required, :except => [ :new, :create ]

    # use Restful ACL to determine whether the user is authorized
    # to access the requested method
    before_filter :has_permission?

    def index
      # you could create a default scope to order by lastname, firstname
      @users = User.all
    end

    def show
      @user = User.find(params[:id])
    end

    def new
      @user = User.new
    end

    def create
      unless logged_in?
        logout_keeping_session!
      end
      @user = User.new(params[:user])
      success = @user && @user.save
      if success && @user.errors.empty?
        unless logged_in?
          # the following applies to guests 
          # who have created accounts for themselves
          self.current_user = @user # !! now logged in
          flash[:notice] = "Thanks for signing up!"
          redirect_to root_path # make this wherever you want new users to go next.
        else
          # this applies to admins who have created accounts
          flash[:notice] = 'New user account created.'
          redirect_to users_path
        end
      else
        flash[:error]  = "Error setting up the account."
        render :action => 'new'
      end
    end

    def edit
      @user = User.find(params[:id])
    end

    def update
      @user = User.find(params[:id])
      if @user.update_attributes(params[:user])
        flash[:notice] = 'Successfully updated account.'
        redirect_to @user
      else
        render :action => 'edit'
      end
    end

  end

To correspond with the controller, the views need some updating. I’m showing one view here–a partial for the form, for use in the new and edit view templates. Mine’s written in Haml, but if you prefer Erb you should be able to figure it out.

app/views/users/_form.html.haml:
  - form_for @user do |f|

    = f.error_messages

    %p
      = f.label :firstname, 'First name'
      %br
      = f.text_field :firstname

    %p
      = f.label :lastname, 'Last name'
      %br
      = f.text_field :lastname

    %p
      = f.label :login, 'Username'
      %br
      = f.text_field :login

    %p
      = f.label :email, 'E-mail'
      %br
      = f.text_field :email

    %p
      = f.label :password
      %br
      = f.password_field :password
      
    %p
      = f.label :password_confirmation, 'Confirm password'
      %br
      = f.password_field :password_confirmation
      
    - if logged_in? && current_user.is_admin?
      %p
        = f.check_box :is_admin
        = f.label :is_admin, "System administrator"

    %p
      = f.submit 'Submit'

I’ll let you figure out the rest of the views–they are straightforward.

Finally, add RESTful routes for the User model:

config/routes.rb:
  map.resources :users

That’s it! Your Users scaffold now allows users to edit their accounts and administrators to add or edit any account. If you’re still using Restful Authentication in your app, give it a try and let me know how it works for you. If you’ve got suggestions for how to improve this code please let me know. If you think it would be helpful, I’ll try to put together a quick demo app with these settings and post it to GitHub sometime this week.

Rails testing made simple

Learn to test Rails apps the way I learned, building up tests step-by-step, in Everyday Rails Testing with RSpec. Expanded to include exclusive content and a complete sample Rails application. Learn more »

Also available on Amazon.com.

blog comments powered by Disqus