3.5. Authentication

Now that you're creating new users and storing their passwords securely, the next step is to allow the user to log in. This involves setting up two new actions in the user controller — login and logout — and setting up partial views to display the login form and logout link.

3.5.1. The Routes

Because you are adding new actions to the RESTful user controller, the place to start is in the routes.rb file. Change the entry for users to this:

map.resources :users, :new => {:login => :post},
    :member => {:logout => :get}

This line adds a new action for login, which operates on a new or unsaved user object, and another action for logout, which operates on a single existing user object. The login action is a POST, because data is being sent to the server, and logout is a GET, which I suppose is arguable but seemed the best choice because no additional data besides the user ID is being sent to the server.

The most commonly used RESTful plugin for authentication, called restful_authentication, does this a bit differently. It creates a Sessions controller where the login method is Sessions#create and logout is Sessions#delete. There's certainly value in maintaining REST consistency, but there's not a whole lot of practical difference between the two designs, unless you have other uses for a Sessions controller.

3.5.2. The Tests

The user tests for password management have already been written. Here are the controller tests for successful login, unsuccessful login, and logout, which are defined in test/functional/users_controller_test.rb:

def test_should_login_succesfully
    post :login, :user => {:username => "ktf", :password => "qwerty"}
    assert_response :success
    assert_equal 1, session[:user_id]
    assert_template "users/_already_logged_in"
  end

  def test_should_not_login_on_bad_credentials
    post :login, :user => {:username => "ktf", :password => "banana"}
    assert_response :success
    assert_nil session[:user_id]
    assert_template "users/_login"
  end

  def test_should_logout
    post :login, :user => {:username => "ktf", :password => "qwerty"}
    assert_equal 1, session[:user_id]
    get :logout, :id => 1
    assert_response :success
    assert_template "users/_login"
    assert_nil session[:user_id]
  end

There's sort of a chicken-and-egg problem with testing a login. You need to have a valid user object in your test/fixtures/users.yml fixture file, but that valid user object needs to have an encrypted password as it's stored in the database. In other words, it needs to look like this:

one:  id: 1
  username: ktf
  first_name: Kermit
  last_name: The Frog
  email: [email protected]
  encrypted_password: 242e401c23ec2612fce9bf19ce3816c77c283d75126c0d5ee0e06897a89a300e
  salt: "_aq0TcTITNE"
  created_at: 2007-07-15 10:54:32
  updated_at: 2007-07-15 10:54:32
  is_active: false

Just using the password field, and expecting Rails to use the user object to create the encrypted field as though it was entered via a form will not work — Rails loads the fixtures to the database directly, without mediation by the ActiveRecord object. What I did was create a user using the actual interface developed in the previous section, and copied that user's salt and encrypted password to the fixture file. This works, but it's less than elegant. You could also use the console to create a user object and generate a valid salt and encrypted password.

A successful login will store the current user ID in the session object at session[:user_id]. Because the session object is usually stored on the server and the user can never manipulate it directly, the session is a reasonably secure place to store user data, although it's a good idea to clear out old sessions to prevent them from living forever. This means that users will have to log in again if they leave the site and come back. Later in the chapter, you'll see a mechanism for securely allowing persistent logins.

In the logout test, you prime the session object by simulating a successful login first, asserting that the login was successful, and then logging out to observe the change. You can also see that there will be two partial templates defined: one displayed with the login form, and the other displayed if the user is already logged in.

3.5.3. The Controlle

The login code goes in the user controller, app/controllers/users_controller.rb, and starts by calling the User.find_by_username_and_password method defined earlier. Here's the code:

def login
    @user = User.find_by_username_and_password(params[:user][:username],
        params[:user][:password])
    if @user
      session[:user_id] = @user.id
      render :partial => "already_logged_in"
    else
      flash[:login_message] = "Login failed"
      @user = User.new
      render :partial => "login"
    end
  end

The user find method either returns the successfully matched user object, or it returns nil. If there's an actual user created, the ID is placed in the session, and the partial for a user who is logged in is rendered. If the method returns nil, a message is put in the flash text for eventual display, and the @user attribute is set to a new, empty user object.

There are a couple of design decisions to be made here. The rendering of just partial templates, and the insistence that the @user variable have a non-nil value, will make more sense when you see how the login form is integrated into the site.

Return a Vague Error Message

When you're returning an error message from a failed login, it's tempting to differ entiate between "unknown username" and "invalid password." This is generally considered a bad idea, because a malicious user can use this message to verify that a username is actually in the system database, making the job of cracking a username/ password pair that much easier.

Despite this, you'll still see systems, even successful web applications, that will make that distinction on a failed login. And I have to say, as a benevolent web user who has dozens of logins on dozens of sites, I appreciate the consideration because it makes my guessing what I was thinking of six months ago when I created that site account that much easier.

But I still would send the vague error message to a user on any site I developed.


The logout action is even simpler. It depends on the following load_user method, which you need to place in app/controllers/application.rb, because other controllers will want it:

def load_user
    @user = User.find_by_id(session[:user_id]) || User.new
  end

Right now, the method either creates a user object from the ID stored in the session, or it creates a blank user object and assigns that to the @user variable. Next, you define the logout method like this:

def logout
    load_user
    flash[:login_message] = "Logged out"
    if @user.id == session[:user_id]
      session[:user_id] = nil
      @user = User.new
      render :partial => "login"
    else
      render :partial => "already_logged_in"
    end
  end

All this does is reset the session and the @user object to default values, and then renders the login partial. One security issue to note here is that because logout is a RESTful action, it is called with a user ID. Because you have the ID used to make the call, you might as well ensure that the user ID from the call actually matches the user ID in the session before performing the logout. It's a good habit to get into — validating that the attributes passed via a URL are what you expect before performing an action—although in this case, it's admittedly hard to see what serious security breach could actually occur.

3.5.4. The Views

The form for logging in and logging out is going to be part of the common layout. In keeping with Rails best practice, it's implemented as a helper method that redirects to the proper partial template based on the user status, although as you saw previously, explicitly calling login or logout bypasses that helper and displays the correct partial template directly from the controller.

Within the layout file app/views/layouts/recipes.html.erb, add the following highlighted code:

<div id="sidebar">
      <ul>
          <li id="login">
                  <h2 class="bg2">Login</h2>
                  <%= render_login_div %>
          </li>
          <li id="menu" class="bg6">

This creates an element in the sidebar, and calls a helper method named render_login_div. Place that method in app/helpers/application_helpers.rb as follows:

def is_logged_in?
    return false unless @user && session[:user_id]
    not @user.username.blank?
  end

  def render_login_div(&proc)
    content_tag("div", :id => "login_form") do
      if flash[:login_message]
        content_tag("div", "#{flash[:login_message]}", :id => "notice")
      end
      if is_logged_in?
        render :partial => "/users/already_logged_in"
      else
        render :partial => "/users/login"
      end
    end
  end

What you want is for a div element with the id login_form to be created, and to display either the login form or the "already logged in" message depending on whether the user is already logged in. This code assumes that the @user object will already exist. To ensure that, add a before_filter to the recipe controller, calling the load_user method you just saw, like this:

before_filter :load_user

To actually generate the HTML text, you'll use your old friend content_tag, last seen supporting metaprogramming in the form builder. The div is created, the flash message is added if needed, and then the if logic redirects the rendering code.

Because the common elements and the display logic have been placed in the helper, both of the view files are quite simple. That's the point. Pure Ruby code in the helper method is much better situated to manage complexity than the mixed HTML and Ruby code of the .erb file.

The login form uses the table form builder, and is rather simple. This is app/views/user/login.html.erb:

<% remote_table_form_for(@user, :url => login_new_user_url,
    :update => "login_form") do |f| %>
  <table>
    <%= f.text_field :username, :size => 10,
          :caption_class => "subtle" %>
    <%= f.password_field :password, :size => 10,
          :caption_class => "subtle" %>
    <%= f.submit "Log In", :class => "subtle" %>
    <%= f.row link_to("New User?", new_user_url, :class => "subtle") %>
  </table>
<% end %>

It's an Ajax remote form, meaning that it will update a specific element within the page — in this case the login_form element that the helper method created to enclose the form. You define the remote_table_form_for helper method in app/helpers/application_helper.rb (it's analogous to the one you defined earlier for non-Ajax form creation):

def remote_table_form_for(name, *args, &proc)
    remote_form_for(name, *convert_args(TabularFormBuilder, args),
        &proc)
  end

There is one difference between the regular and Ajax versions, though. Remember when I said that the regular version put the form tag inside the table tag? It turns out that this has no functional affect on a regular form, but at least one browser can't handle it in an Ajax form — for some reason it refuses to match the attributes to the form. So, in the best tradition of web programming, you can work around unexpected browser behavior by putting the table tags inside the actual form_for block, which organizes things more in line with browser expectations.

Other than that, the form is simple, using the features of the custom form builder to remove the clutter. The generic row mechanism is used to add a non-form element link to create a new user.

After a successful login, display the active user's name and the logout option. This is the _already_logged_in.html.rb partial:

<div>
  You are logged in as <%= @user.full_name %>.
</div>
<div>

<%= link_to_remote "Log Out", {:url => logout_user_url(@user),
         :method => :get,
         :update => "login_form"},
         :class => "subtle" %>
</div>

The result is two displays in the sidebar. Figure 3-2 shows the first display, for the login form.

Figure 3.2. Figure 3-2

Then, once the user has logged in, he'll see the second display, shown in Figure 3-3.

Figure 3.3. Figure 3-3

3.5.5. Using Authentication

Now that the user has to authenticate, there are some features that should be limited. For example, only a user who has logged in should be able to enter a recipe, and only the user who initially entered the recipe should be able to edit it. After writing the code so far, adding these features is almost staggeringly simple.

Put the following methods in application_helper.rb:

def if_is_current_user(user_id)
    yield if is_logged_in? and user_id == @user.id
  end

  def if_is_logged_in
    yield if is_logged_in?
  end

Both of these methods are block helpers — helper methods that expect to be called with a block of the ERB template as an argument. In these cases, you are using the helper to remove the conditional logic from the view template. There are a couple of advantages to doing this, the simplest of which is basic Don't Repeat Yourself (DRY) — if the logic changes or gets more complex, then the code needs to be touched only in one place. Also, the named helper method will generally be better at revealing intent than the if statement.

Each of the helper methods executes the block only if the conditional is true. This means that the HTML or ERB code inside the block is only printed to the response if the condition is true.

The first place you'll want to use these helpers is in the recipe listing. The new recipe link should be available only if the user has logged in, and the actions should be available only if the user is actually responsible for that recipe. Here's what the new code in app/views/recipes/index.html.erb looks like, with a couple of other cosmetic changes:

<% @title = "Recipes" %>
<table width="75%">
  <tr><th>Title</th></tr>
  <% for recipe in @recipes %>
    <tr>
      <td><%= link_to(h(recipe.title), recipe) %></td>
      <% if_is_current_user recipe.user_id do %>
        <td><%= link_to 'Edit', edit_recipe_path(recipe) %></td>
        <td>
          <%= link_to 'Destroy', recipe, :confirm => 'Are you sure?',
                 :method => :delete %>
        </td>
      <% end %>
    </tr>
  <% end %>
</table>
<br />
<% if_is_logged_in do %>
  <%= link_to 'New recipe', new_recipe_path %>
<% end %>

Each block helper is used in this view, and these helpers are used just the same as any other method that takes a block, except that in this case, the blocks are composed of arbitrary ERB text placed between the do and end statements. Like an if or for statement, the helper blocks take the <% execute marker and not the <%= execute and print marker.

The other place where a user can edit is in the recipe show page, where you allowed the inline change to an ingredient. That also needs a guard clause around it. Here's the relevant part of that view:

<div class="ingredients">
  <h2>Ingredients</h2>
  <% for ingredient in @recipe.ingredients %>
    <div class="ingredient">
      <span id="ingredient_<%= ingredient.id %>">
        <%= h ingredient.display_string %>
      </span>

<% if_is_current_user @recipe.user_id do %>
        <span class="subtle" id="edit_<%= ingredient.id %>">
          <%= link_to_remote "Edit",
                 :url => remote_edit_recipe_ingredient_path(
                    @recipe, ingredient),
                 :method => :get,
                 :update => "ingredient_#{ingredient.id}"%>
        </span>
      <% end %>
    </div>
  <% end %>
</div>

Again, the block helper ensures that the edit field will display only if the current user is actually the owner of the recipe.

3.5.6. Adding User Roles

I realize that this security model is a little simplified. At the very least, you'd want an administrator level that could edit any recipe, and you might also want some kind of friend or group access. One mechanism for adding role-based access to your Rails application is by using the plugin simple_access_control, which works with the custom authentication system built in to Soups OnLine. The plugin is available via the following:

$ script/plugin install -x
http://mabs29.googlecode.com/svn/trunk/plugins/simple_access_control

For the plugin to be of use, there are basically three requirements:

  1. The user class must respond to a roles method. The mechanism recommended by the plugin is to create a roles database table with a single column, using the following in a migration:

    create_table "roles", :force => true do |t|
        t.column "title", :string
      end

  2. Have a many-to-many relationship between the User model and the Role model, which involves creating a standard join table for roles_models. That's overkill for Soups OnLine, where there are probably only two or three levels of access and a user would exclusively belong to one of them. This being Rails, you could achieve roughly the same effect by adding a role attribute to the users table and doing something like this:

    def roles
      [Role.new(role)]
    end

  3. And then, elsewhere, define the following:

    Class Role
      attr_accessor :title
      def intialize(role)

    @title = role
      end
    end

There are also two methods that need to be accessible in your controllers, generally by being in the application.rb file. The method current_user needs to return the user object that is current in the session, and the method logged_in? needs to return true or false based on whether a user is currently logged in (if you are using the restful_authentication plugin, those methods are defined for you by that plugin). That functionality exists in the system, but under different names. The following changes should go into application.rb:

def current_user
    load_user
  end

  def logged_in?
    user = load_user
    return false unless user && session[:user_id]
    not user.username.blank?
  end

With that accomplished, the plugin allows you to set access rules at either the controller level or around specific blocks of view code. At the controller level, there is a new method, access_rule, that looks like this:

access_rule "user", :only => [:index, :show]

The first argument is the role being measured up for the rule. You can allow the same rule to apply to multiple roles by using a pseudo-Boolean syntax, "user || admin". The :only parameter is a list of all controller actions that should be accessible to a user who has that role. If a user has multiple roles, that user has access to any controller action accessible to any of those roles. If there are no rules defined for a specific role, it is assumed to have access to all actions.

If the user attempts to access an action he or she does not have access to, the plugin redirects the call to a permission_denied method, which you need to define either in each controller or in application.rb. The typical behavior of this method is to put an alert message in the flash, and redirect the user someplace harmless. You should also end the user's session as a security measure. There's also a permission_granted callback method should you have some specific action to take when the user is allowed to do something (for example, logging).

Within a view, you have access to the following restrict_to helper method, which takes an access rule and a block:

<% restrict_to 'admin' do %>
    <%= link_to "Edit", edit_recipe_url(@recipe) %>
  <% end %>

The block is executed only if the current user has one of the roles listed in the rule. A related method, has_permission?, takes a rule as an argument and returns a Boolean suitable for use in if statements or clauses.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.255.127