Quest for the Holy Rails

May 4, 2007

Our Journey Begins: Players

Filed under: pente, rails — Jake Brownson @ 8:24 pm

I’ve introduced the concept of test driven development and REST and I’m finally ready to start writing some code on my Pente project. In this post I will setup the project, and generate and customize the player resource.

I’m going to assume that you’ve covered the basics already so I’ll go quickly over some of the basics.

Note: Since we’re using REST Rails must be at version 1.2 or better

The first step is to create your rails project:

jake@jake-laptop:~/src$ rails pente -d sqlite3
      create
      create  app/controllers
      create  app/helpers
      ... snip ...
      create  log/production.log
      create  log/development.log
      create  log/test.log
jake@jake-laptop:~/src$

I used “-d sqlite3” so it would use sqlite3 for the databases. I find it much easier to develop using sqlite3 since you don’t need to setup mysql. When I deploy the app I’ll evaluate the different database options and might change the database then, but for now it’s sqlite3.

Next let’s install a plugin from Rick Olson called restful_authentication:

jake@jake-laptop:~/src/pente$ script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/
+ ./restful_authentication/README
+ ./restful_authentication/Rakefile
... snip ...
+ ./restful_authentication/generators/authenticated/templates/unit_test.rb
+ ./restful_authentication/install.rb
Restful Authentication Generator
====

This is a basic restful authentication generator for rails, taken from acts as authenticated.  Currently it requires Rails 1.2 (or edge).

To use:

  ./script/generate authenticated user sessions --include-activation

The first parameter specifies the model that gets created in signup (typically a user or account model).  A model with migration is created, as well as a basic controller with the create method.

The second parameter specifies the sessions controller name.  This is the controller that handles the actual login/logout function on the site.

The third parameter (--include-activation) generates the code for a ActionMailer and its respective Activation Code through email.

You can pass --skip-migration to skip the user migration.

From here, you will need to add the resource routes in config/routes.rb.

  map.resources :users, :sessions

Also, add an observer to config/environment.rb if you chose the --include-activation option
  config.active_record.observers = :user_observer # or whatever you named your model

jake@jake-laptop:~/src/pente$

This plugin adds a generator for making RESTful authentication systems. Let’s use it to add a players resource using this generator:

jake@jake-laptop:~/src/pente$ script/generate authenticated player sessions -p

----------------------------------------------------------------------
Don't forget to:

  - add restful routes in config/routes.rb
    map.resources :players, :sessions
    map.activate '/activate/:activation_code', :controller => 'players', :action => 'activate'

Try these for some familiar login URLs if you like:

  map.signup '/signup', :controller => 'players', :action => 'new'
  map.login  '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'

----------------------------------------------------------------------

      exists  app/models/
      exists  app/controllers/
      ... snip ...
      create  db/migrate
      create  db/migrate/001_create_players.rb
jake@jake-laptop:~/src/pente$

It’s not hard to think of how to make players RESTful. Players map to resources quite easily. The authentication itself it a little trickier to think of RESTfully. Think about what you’re really doing when you login. You’re creating a login session. When you logout you’re destroying a login session. The login session itself is the REST resource.

Let’s add the REST resource to config/routes.rb, and while I’m at it I’ll clean it up a bit:

ActionController::Routing::Routes.draw do |map|
  map.resource :players, :sessions
  # map.connect '', :controller => "welcome"
  map.connect ':controller/:action/:id.:format'
  map.connect ':controller/:action/:id'
end

Now let’s run the players migration generated for us:

jake@jake-laptop:~/src/pente$ rake db:migrate
(in /home/jake/src/pente)
== CreatePlayers: migrating ===================================================
-- create_table("players", {:force=>true})
   -> 0.0072s
== CreatePlayers: migrated (0.0073s) ==========================================

jake@jake-laptop:~/src/pente$

And now let’s run the generated tests to make sure everything is working:

jake@jake-laptop:~/src/pente$ rake
(in /home/jake/src/pente)
/usr/bin/ruby1.8 -Ilib:test "/var/lib/gems/1.8/gems/rake-0.7.3/lib/rake/rake_test_loader.rb" "test/unit/player_test.rb"
Loaded suite /var/lib/gems/1.8/gems/rake-0.7.3/lib/rake/rake_test_loader
Started
.............
Finished in 0.088721 seconds.

13 tests, 26 assertions, 0 failures, 0 errors
/usr/bin/ruby1.8 -Ilib:test "/var/lib/gems/1.8/gems/rake-0.7.3/lib/rake/rake_test_loader.rb" "test/functional/sessions_controller_test.rb" "test/functional/players_controller_test.rb"
Loaded suite /var/lib/gems/1.8/gems/rake-0.7.3/lib/rake/rake_test_loader
Started
..............
Finished in 0.127973 seconds.

14 tests, 26 assertions, 0 failures, 0 errors
/usr/bin/ruby1.8 -Ilib:test "/var/lib/gems/1.8/gems/rake-0.7.3/lib/rake/rake_test_loader.rb"
jake@jake-laptop:~/src/pente$

And there it is. We’ve got a fully working authentication and signup mechanism! Let’s take a look at what was generated to understand what is really going on.

First Let’s look at the migration:

class CreatePlayers  true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime

    end
  end

  def self.down
    drop_table "players"
  end
end

Some of the columns’ functions are pretty obvious (login, email, created_at, updated_at), but if you haven’t designed an authentication system before (I hadn’t really either) the other columns might be a bit of a puzzle.

  • crypted_password – Well it’s not really encrypted, it’s a hash of the password. A hash cannot be directly decrypted back to the original password, but you can apply the same hash algorithm to the password given at login and determine if the hashes match. This way we don’t store the users’ actual passwords which makes for better security. Even if the hash database were stolen it would take a lot of processing power to determine even one user’s true password.
  • salt – If the hashed password list is somehow stolen it is possible to use precomputed hash dictionaries to get weak passwords that are dictionary words or other obvious things. when the user picks or changes a password a random string is generated and stored in the salt column. Before the password hash is generated the salt is concatenated with the plaintext password. This means that even though two users might have the same password the randomly generated salt would mean they have different hashes. Therefore you can’t just calculate the hash for “password” and scan for matching hashes.
  • remember_token – Instead of logging in every time the user visits the site a randomly generated token can be stored in a cookie in the user’s browser. This cookie is checked when the user visits the site and the user is automatically authenticated if the match.
  • remember_token_expires_at – Whenever the token is checked this column is also checked to make sure that the token hasn’t expired. If it has then the user must be authenticated through standard means.

The generator adds a couple of files in lib for us. libs/authenticated_system.rb contains methods useful for inclusion in methods, controllers and views. lib/authenticated_test_helper.rb contains methods useful for testing controllers that require authentication.

Here are some of the highlights:

lib/authenticated_system.rb

    def logged_in?
      current_player != :false
    end
...
    def current_player
      @current_player ||= (session[:player] && Player.find_by_id(session[:player])) || :false
    end

The above methods are commonly referenced in the view and controller to determine if the user is logged in and if so who’s logged in.

lib/authenticated_system.rb

    def login_required
      username, passwd = get_auth_data
      self.current_player ||= Player.authenticate(username, passwd) || :false if username && passwd
      logged_in? && authorized? ? true : access_denied
    end
...
    def login_from_cookie
      return unless cookies[:auth_token] && !logged_in?
      user = Player.find_by_remember_token(cookies[:auth_token])
      if user && user.remember_token?
        user.remember_me
        self.current_player = user
        cookies[:auth_token] = { :value => self.current_player.remember_token , :expires => self.current_player.remember_token_expires_at }
        flash[:notice] = "Logged in successfully"
      end
    end

The above methods are intended to be used as before_filters for classes that need authentication.

module AuthenticatedTestHelper
  # Sets the current player in the session from the player fixtures.
  def login_as(player)
    @request.session[:player] = player ? players(player).id : nil
  end
...
  # Assert the block redirects to the login
  #
  #   assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
  #
  def assert_requires_login(login = nil)
    yield HttpLoginProxy.new(self, login)
  end

The above methods can be very useful when testing controllers that use authentication.

The generator gave us two resources. The sessions resource is a sort of “virtual” resource that doesn’t actually have a table in the database or a model but does have a controller and views. The players resource is more standard and has a table, model controller, and views.

We should move the includes from the two generated controllers to the application controller as shown. This way all controllers will have access to the helper methods:

class ApplicationController < ActionController::Base
  include AuthenticatedSystem
  before_filter :login_from_cookie

  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_pente_session_id'
end

We should also move the includes from the two generated controller functional tests to the test_helpers.rb so they will be available from all tests.

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'

class Test::Unit::TestCase
  ...snip...

  # Add more helper methods to be used by all tests here...
  include AuthenticatedTestHelper
end

The last thing the generator gave us was a set of tests and test fixtures to exercise the generated code. These are the tests we ran earlier.

We can experiment with signing up for accounts and logging in by starting WEBrick:

jake@jake-laptop:~/src/pente$ script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2007-05-05 18:03:17] INFO  WEBrick 1.3.1
[2007-05-05 18:03:17] INFO  ruby 1.8.5 (2006-08-25) [i486-linux]
[2007-05-05 18:03:17] INFO  WEBrick::HTTPServer#start: pid=8278 port=3000

Open your browser and load http://localhost:3000/players/new and create an account. You can now log in at http://localhost:3000/sessions/new. There isn’t much to see yet when you log on. You will simply be redirected to the default Rails page. If you use an invalid account to login you will not be redirected.

4 Comments »

  1. Excellent tutorial – many thanks.

    I ran into an issue with running the tests:

    http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/2f49a6338ddfdd48

    A workaround is posted, which I may try.

    Comment by Rich Apodaca — August 5, 2007 @ 3:11 pm

  2. Glad you found it helpful Rich!

    Comment by jbrownson — October 15, 2007 @ 3:55 am

  3. This is awesome !!! Good work

    Comment by Maria — February 6, 2009 @ 6:24 pm

  4. Hello webmaster
    I would like to share with you a link to your site
    write me here preonrelt@mail.ru

    Comment by Alexwebmaster — March 3, 2009 @ 2:13 pm


RSS feed for comments on this post. TrackBack URI

Leave a reply to Rich Apodaca Cancel reply

Create a free website or blog at WordPress.com.