Saturday, December 11, 2010

Adding Authentication with Devise 1.2.rc to a Rails 3.0.3 Application

NB: This post describes my experience following Ryan Bates' Railscast on installing Devise. He uses Rails 3.0.0 with Devise 1.1.rc0; I use Rails 3.0.3 with Devise 1.2.rc. As such my experience differs slightly from the screencast: I change one script command and experience two fewer problems.

Introduction: Choosing Devise

I want wikidiscography.com data to be available to anyone without requiring a log-in, but I only want registered users to be able to add, remove, and edit data. I'm not that this is the best idea; after all, wikipedia doesn't require registration. One good reason may be that correctly entering discographical data may be difficult, so I might want to have some control over who gets editing privileges. For no rigorous reason besides reading a few blog posts and checking out ruby-toolbox.com, I chose Devise. In this blog post, I'll go over how I install it to wikidiscography.com. The first thing I did was check out the ASCIIcasts on Devise and on customizing Devise. Both of these are based on Ryan Bates' railscasts.com screencasts. Thank you Ryan.

Create a new git branch

Before I change any application code, I want to create a new branch on git:
$ git branch devise
$ git checkout devise

Check that you're in the correct branch with
$ git branch
and see that the asterisk is next to the "devise" branch. Now we can safely delete files and destroy the application, and if we make irrecoverable mistakes, we can just delete the branch and be back to where we were.

Remove previous authentication code

Since I've been going through Michael Hartl's Ruby on Rails tutorial which includes a section on creating a custom authentication solution from scratch, the first thing I have to do is go through the application and remove all that code. This includes the following places:

  • user.rb
  • users_controller.rb
  • specs
  • helpers
  • Gemfile (remove unnecessary gems)
  • routes
  • layouts (remove signup link from _footer)
  • database tables (probably a user table and the associated routes; remove them by migrating down and the deleting the migrations)
Once that is done, restart the server and poke around the application to make sure all the code from the previous authentication solution has been removed. From the command line, run $ Rspec spec for one more check. If those tests pass, it's time to move on to installing Devise.

Installing Devise

From here on, I'll be following the Devise ASCIIcast, but making it specific to wikidiscography.com.

Which version?

First, which version should we install? Since this is a Rails 3 application, we should probably go with the latest version currently available. If it were a Rails 2.3 app, you should use Devise 1.0.6, according to Ryan Bates. Since a few months have passed since the Railscast was released, we should look at the Devise changelog to see what the latest version offers. The latest release as of this writing is 1.2.rc. We'll go with that for now, and if it breaks something we may need to downgrade later. (Update: It didn't break anything.)

Add it to the Gemfile

Add the appropriate line to the Gemfile, and then let Bundler do the work of finding and installing it: Gemfile:
gem 'devise', '1.2.rc'

Command line from within wikidiscography folder:
$ sudo bundle install
(This should take about 30 seconds.) Among all the rest of the output that this generates, I get the following lines:

Installing bcrypt-ruby (2.1.2) with native extensions  Using bundler (1.0.0)  Installing orm_adapter (0.0.3)  Installing warden (1.0.2)  Installing devise (1.2.rc) 

This tells me that Bundler has installed not only Devise, but its dependencies, which are Warden, orm_adapter, and bcrypt-ruby. I don't know what these are, nor do I care. According to the ASCIIcast, the next step is to run the installation generator.

$ rails generate devise_install
For me, this returned the following error:

Could not find generator devise_install
This is why it's a good idea to use the same version of software that the tutorial uses. It turns out that the newer version of Devise that I installed (1.2.rc) has a slightly different installation script, as noted in the installation notes on Devise's GitHub page. The new install command is:

$ rails generate devise:install
This creates the files config/initializers/devise.rb and config/locales/devise.en.yml. It also presents the user with the following instructions:

===============================================================================

Some setup you must do manually if you haven't yet:

  1. Setup default url options for your specific environment. Here is an
     example of development environment:

       config.action_mailer.default_url_options = { :host => 'localhost:3000' }

     This is a required Rails configuration. In production it must be the
     actual host of your application

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root :to => "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

===============================================================================
Those are pretty good instructions, so before moving on, we follow them.

Follow the 3 installation steps

1. Copy the line in the instructions into the file config/environments/development.rb:

config.action_mailer.default_url_options = { :host => 'localhost:3000' }

2. Check the file config/routes.rb to see if there is a
root :to => ... root :to => "pages#home"
, so step 2 is already complete.
3. Check the file app/views/layouts/application.html.erb to see if we have flash messages there. Wikidiscography has the following line:

<%= flash[:notice] %>

but it doesn't have an "alert" line. I don't know if alert is redundant or new to Rails 3 or what, but I'm going to add the line to my application layout anyway because I want Devise to work. I'll remove code later if I can. So now my application layout has the following two lines:

<%= flash[:notice] %>
<%= alert %> 

As far as I can tell, we have satisfied the 3 requirements presented by the Devise installation script.

Let Devise Create a User Model

(I'm still following the ASCIIcast.) Now that Devise has been properly installed (I hope), from the command line type:

$ rails generate devise User
For wikidiscography, this creates the following output:

invoke  active_record
      create    app/models/user.rb
      invoke    test_unit
      create      test/unit/user_test.rb
      create      test/fixtures/users.yml
      create    db/migrate/20101213002243_devise_create_users.rb
      insert    app/models/user.rb
       route  devise_for :users



Notice that instead of generating Test::Unit files as in the ASCIIcast, it invoked Rspec and generated Rspec specs. That's because I previously set up Rspec as my testing environment. Besides that difference, everything seems to be working the same. It also generated a user.rb file at /app/models/user.rb, and a database migration at /db/migrate/20101211053242_devise_create_users.rb. These files give us four modules by default: database_authenticatable, recoverable, rememberable, and trackable. I don't really know or care what these do at the moment, so I'm going to accept the defaults and run the migration.

Migrate the database

From the command prompt, type:

$ rake db:migrate
This produces the following output, showing a new users table, and indexes on two columns:

==  DeviseCreateUsers: migrating ==============================================
-- create_table(:users)
   -> 0.0145s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0327s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0024s
==  DeviseCreateUsers: migrated (0.0502s) =====================================

Once you've migrated the database, restart the server.

Check out the new routes

At the command line, type in

$ rake routes

to see a list of all routes available to the application. Notice the routes associated with the controller "devise":

new_user_session GET    /users/sign_in(.:format)       {:action=>"new", :controller=>"devise/sessions"}
            user_session POST   /users/sign_in(.:format)       {:action=>"create", :controller=>"devise/sessions"}
    destroy_user_session GET    /users/sign_out(.:format)      {:action=>"destroy", :controller=>"devise/sessions"}
           user_password POST   /users/password(.:format)      {:action=>"create", :controller=>"devise/passwords"}
       new_user_password GET    /users/password/new(.:format)  {:action=>"new", :controller=>"devise/passwords"}
      edit_user_password GET    /users/password/edit(.:format) {:action=>"edit", :controller=>"devise/passwords"}
                         PUT    /users/password(.:format)      {:action=>"update", :controller=>"devise/passwords"}
cancel_user_registration GET    /users/cancel(.:format)        {:action=>"cancel", :controller=>"devise/registrations"}
       user_registration POST   /users(.:format)               {:action=>"create", :controller=>"devise/registrations"}
   new_user_registration GET    /users/sign_up(.:format)       {:action=>"new", :controller=>"devise/registrations"}
  edit_user_registration GET    /users/edit(.:format)          {:action=>"edit", :controller=>"devise/registrations"}
                         PUT    /users(.:format)               {:action=>"update", :controller=>"devise/registrations"}
                         DELETE /users(.:format)               {:action=>"destroy", :controller=>"devise/registrations"}
                    root        /(.:format)                    {:action=>"index", :controller=>"welcome"}

Where did all these routes come from? Check out the /config/routes.rb file and notice the line that the devise generator added at the top:

devise_for :users
Again, I don't recognize that syntax, I don't understand how it works, but I'll accept the routes it gives me and happily move on. If you really want to dig into it, check out the file /usr/lib/ruby/gems/1.8/gems/devise-1.2.rc/lib/devise/rails/routes.rb. It's well commented with detailed instructions on how to read and modify routes. Good luck.

Use the new sign_up route

(Still following the ASCIIcast.) At this point, Devise is ready to go. Navigate to /users/sign_up and we'll see a sign-up form. But wait a minute: The Devise generator did not generate any view files, and I certainly never made a sign-in form. So where is this coming from? To see the answer, take a look at the server console output:


Started GET "/users/sign_up" for 127.0.0.1 at Sun Dec 12 19:29:51 -0500 2010
  Processing by Devise::RegistrationsController#new as HTML
Rendered /usr/lib/ruby/gems/1.8/gems/devise-1.2.rc/app/views/devise/shared/_links.erb (1.9ms)
Rendered /usr/lib/ruby/gems/1.8/gems/devise-1.2.rc/app/views/devise/registrations/new.html.erb within layouts/application (56.0ms)
Completed 200 OK in 65ms (Views: 62.6ms | ActiveRecord: 0.0ms)

This shows us a few things: First, it says "Processing by Devise::RegistrationController#new". I never made a "registration controller", but apparently by pointing the application to /users/sign_up it called the "new" method on this phantom controller. Second, the penultimate line gives us more information: "Rendered /usr/lib/ruby/gems/1.8/gems/devise-1.2.rc/app/views/devise/registrations/new.html.erb". There's the answer: The sign-up form is in the gem subdirectory. So if I navigate to usr/lib/ruby/gems/1.8/gems/devise-1.2.rc/app/, I see all the files that Devise uses to work its magic. At this point I'm content to let it be magical without deeply understanding it.

Try signing up

I try out the sign-up form by putting in my name and email address and submitting the form. It works! And it displays a flash message that reads "Welcome! You have signed up successfully." I notice that is also displayed an empty flash[:notice] message, but that is probably something in my own code, and something to debug later. For now, since we know user registration is functioning properly, we move forward and test out other features.

Try signing out

Now that I'm registered and signed in, I click around a few pages, and then try signing out. There's no "sign out" button because I never created one, but as Ryan Bates points out via the ASCIIcast, you can sign out by pointing the browser to the sign-out route. Which route is that, exactly? Find it by referencing the routes output from

$ rake routes
The line we need is "/users/sign_out", so type that into the address bar and see what happens.

After visiting /users/sign_out
Voila, we are signed out and given a friendly flash message: "Signed out successfully."

Sign in again

Since we now have a user in the database, we go back to /users/sign_in to sign in using the credentials we supplied when signing up. At this point in the ASCIIcast, Ryan Bates describes an error caused by Rails and how to fix it. Since that episode, the bug has been fixed, and everything just works. Devise is now able to sign in an existing user successfully.

Git commit; git push

Now that everything seems to be working properly, I want to commit my changes and and merge back into the master branch.

$ git commit -am "installed and configured Devise"
$ git checkout master
$ git merge devise
$ git branch -d devise

In the code above, I first committed my changes and added a custom message; then I moved from the devise branch to the master branch using the "checkout" command; then from the master branch I called for the devise branch to merge into the master branch; then I deleted the devise branch.

Done. This got us most of the way through the ASCIIcast, but with considerably fewer problems. This is because we are using a newer version of Rails (3.0.3 instead of a 3 release candidate) and a new version of Devise (1.2.rc instead of 1.1.rc0).

Next post will deal with actually using Devise and adding real user authentication.

No comments:

Post a Comment