Web Application Development Practical: Advanced Ruby on Rails - User authentication

This practical is continuous from previous practical, by providing a login facility based on user accounts. Users will log in with a unique username and password, as in most commercial and community web sites. Before doing this practical you should have finished previous practical. .

 

Step 1) Enter the cookbook directory of the application. we’re going to need a controller for creating users and so we’ll create a Users Controller and give it a new action.

rails g controller users new

In the above rails g” is a short form for “rails generate”.

Step 2) We’ll need a User model to go with the controller in order to store the users’ email addresses and passwords. For obvious reasons we should never store passwords as plain text so instead we’ll store a password hash and a salt.

rails g model user email:string password_hash:string password_salt:string

Step 3) Now we’ve created the model we’ll migrate the database to create the users table.

rake db:migrate

Step 4)Next we’ll write the code in the app/controllers/user_controller.rb file for the new and create actions in the UsersController.

  class UsersController < ApplicationController  
  
    def new  
      @user = User.new  
    end 

    # POST users
    def create  
      @user = User.new(params[:user])  
      if @user.save  
        redirect_to :sign_up, :notice => "Signed up!"  
      else  
        render "new"  
      end  
    end 
   
  end  

Step 5) We’ll write that “new” template now. In the folder cookbook/app/views/users, find a file named new.html.erb. Rewrite this file, and make it have a form that has email, password and password_confirmation fields along with some code for showing any validation errors.

<!-- /app/views/users/new.html.erb -->
<h1>Sign Up</h1>  

  <%= form_for @user do |f| %>  

  <% if @user.errors.any? %>  

    <div class="error_messages">  

      <h2>Form is invalid</h2>  

      <ul>  

        <% for message in @user.errors.full_messages %>  

          <li><%= message %></li>  

        <% end %>  

      </ul>  

    </div>  

  <% end %>  

  <p>  

    <%= f.label :email %><br />  

    <%= f.text_field :email %>  

  </p>  

  <p>  

    <%= f.label :password %><br />  

    <%= f.password_field :password %>  

  </p>  

  <p>  

    <%= f.label :password_confirmation %>  

    <%= f.password_field :password_confirmation %>  

  </p>  

  <p class="button"><%= f.submit %></p>  

<% end %>  

Our User model doesn’t have password or password_confirmation attributes but we’ll be making accessor methods inside the User model to handle these.

Step 6)We’ll make a couple of changes to the routes file next. The controller generator has generated the following route.

get "users/new" 

We’ll change this route in file config/routes.rb to /sign_up, have it point to users#new and give it a name of "sign_up". We’ll also create a root route that points to the signup form. Don't forget to delete index.html in the cookbook/public folder. Finally we’ll add a users resource so that the create action works.

  get "sign_up" => "users#new", :as => "sign_up"  
  root :to => "users#new"  
  resources :users

Visit http://localhost:3000/sign_up, and try to sign up as a new user. We will get some errors. This is because we have a password field on the form for a User but no matching field in the database and therefore no password attribute in the User model. You may get different errors depending on which version of rails you are using, in this case don't worry and go ahead.

In file app/models/user.rb, we’ll create that attribute in the model now along with an attribute to handle the password_confirmation field. We also append these two attributes in the attr_accessible section. For password_confirmation we can use validates_confirmation_of which will also check that the password and the validation match. Now it is also a good idea to add some other validation to the form to check for the presence of the email address and password and also the uniqueness of the password.

class User < ActiveRecord::Base 
   attr_accessible :email, :password_hash, :password_salt, :password, :password_confirmation
 
  attr_accessor :password,:password_confirmation 
  
  validates_confirmation_of :password  
  
  validates_presence_of :password, :on => :create  
  
  validates_presence_of :email  
  
  validates_uniqueness_of :email  
end 

When we created the User model we created password_hash and password_salt fields in the database to hold the encrypted version of the password. When the form is submitted we’ll have to encrypt the value in the password field and store the resulting hash and salt in those two fields. A good way to encrypt passwords is to use bcrypt and we’ll use the bcrypt-ruby gem to do this in our application. First we’ll add a reference (see the last line of the following statements) to the gem in the Gemfile (in the cookbook folder). 

source 'http://rubygems.org'  
gem 'rails', '3.2.8'  
gem 'sqlite3'  
gem 'bcrypt-ruby', :require => 'bcrypt'

Then run the bundle install command to make sure that the gem is installed. You need to restart the web server after the installation. (You may have some problems when installing this gem, for instance, if you are using a mac, make sure you installed xCode, which must include the command line tool in order to compile the package)

Step 7) Next we’ll modify the User model ( app/models/user.rb) so that it encrypts the password before it’s saved. We’ll do this by using a before_save callback that will calls a method called encrypt_password that we’ll write soon. This method will check that the password is present and if it is it will generate the salt and hash using two BCrypt::Engine methods, generate_salt and hash_secret.

 class User < ActiveRecord::Base  
  attr_accessible :email, :password_hash, :password_salt, :password, :password_confirmation

  attr_accessor :password,:password_confirmation

  before_save :encrypt_password  

  validates_confirmation_of :password

  validates_presence_of :password, :on => :create

  validates_presence_of :email

  validates_uniqueness_of :email

  def encrypt_password

    if password.present?

      self.password_salt = BCrypt::Engine.generate_salt

      self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)

    end  

  end 

end

Now when a user signs up the password_hash and password_salt will be stored in the database. If we visit the signup form now it works and if we fill in the form correctly we’ll be redirected to the homepage.

Logging In

Step 8) We’re halfway there now. Users can sign up but they can’t yet sign in. We’ll fix that now, adding a new method called login, process_login and my_account as follow:

  class UsersController < ApplicationController  
  
  def new  
      @user = User.new  
  end 
   
    def create  
      @user = User.new(params[:user])  
      if @user.save  
        redirect_to :log_in, :notice => "Signed up!"  
      else  
        render "new"  
      end  
    end 
  def login  
  
  end  
      
  def process_login  

    user = User.authenticate(params[:email], params[:password])  
  
   if user  
    
      session[:user_id] = user.id  
    
      redirect_to :my_account, :notice => "Logged in!"  
    
   else  
   
      flash.now.alert = "Invalid email or password"  
   
      render "login"  
   
    end  

  end  
  
  def my_account
     
      if session[:user_id] != nil
     
         @sessName = User.find(session[:user_id]).email
     
      else
      
         @sessName = "Guest"
      
      end
  
  end
 
  end  

In the folder app/views/users/, create a file named login.html.erb. Inside this file we’ll create the form for signing in.

<!-- app/views/users/login.html.erb -->
<h1>Log in</h1>
<%= form_tag :action => 'process_login' %>
<p>
<%= label_tag :email %><br />
<%= text_field_tag :email, params[:email] %>
</p>
<p>
<%= label_tag :password %><br />
<%= password_field_tag :password %>
</p>

<%= submit_tag "log in"%>

Then we create very simple view for my_account:

<!-- app/views/users/my_account.html.erb -->
<h1>Account Info</h1>

<p>Your username is <%= @sessName %>

<br>
<a href="log_out">Log out</a>

We’ll need to make some changes to the routing here( config/routes.rb), too. 

  get "log_in" => "users#login", :as => "log_in"  
  get "my_account" => "users#my_account", :as => "my_account"
  get "sign_up" => "users#new", :as => "sign_up"  
  root :to => "users#new"  
  resources :users do    
    post 'process_login', :on => :collection 
 end


Step 9)
Now we need to write the User.authenticate method ( app/models/user.rb). It will try to find a user by the email address that was passed in. If it finds one it will encrypt the password from the form the same way we did when the user signed up, using that user’s password_salt. If the hash from the password matches the stored hash then the password is correct and the user will be returned, otherwise it returns nil. The else statement isn’t really necessary in Ruby as nil is returned anyway but it’s added here for clarity.

def self.authenticate(email, password)  
    user = find_by_email(email)  
    if user && user.password_hash == BCrypt::Engine.hash_secret(password,
user.password_salt)  
      user  
    else  
      nil  
    end   
end

Before we test this out we’ll modify the application’s layout file ( app/views/layouts/application.html. erb) so that the flash messages are shown.

 
<!DOCTYPEhtml>
<html>
<head>
<title>Cookbook</title>
<%=stylesheet_link_tag:all%>
<%=javascript_include_tag:defaults%>
<%=csrf_meta_tag%>
</head>
<body>
<%flash.eachdo|name,msg|%>
<%=content_tag:div,msg,:id=>"flash#{name}"%>

<%end%>
<%=yield%>
></body>
</html>

If we try to log in with an invalid username or password then we’ll see the login form again and now we’ll see the flash message telling us that our login was invalid.

Logging Out

Step 10) This all works but we’ll need a way to log out as well. The first thing we’ll do to implement this is to adding a new route.

  get "log_in" => "users#login", :as => "log_in"  
  get "my_account" => "users#my_account", :as => "my_account"
  get "log_out" => "users#logout", :as => "log_out"  
  get "sign_up" => "users#new", :as => "sign_up"  
  root :to => "users#new"  
  resources :users do 
 post 'process_login', :on => :collection 
 end

This route points to the UsersController’s logout action. The logout action will be defined in app/controllers/users_controller.rb. This action will log the user out by removing the user_id session variable and then redirecting to the home page.

Now if you try to use the my_account action without being logged in, you'll be redirected to the login page.

def logout
  session[:user_id] = nil
  redirect_to :log_in , :notice => "Logged out!"
end 

We can try this out by visiting /log_out. When we do we’ll be redirected back to the home page and we’ll see the “Logged out!” flash message.

There's just one missing piece: you can visit the my_account action even if you're not logged in. Becuase we don't have a way to close off an action to unauthenticated users.

Step 11) Edit app/controllers/application_controller. rb to define the three actions:

   class ApplicationController < ActionController::Base
    
  protected
    
    def login_required
      return true if User.find_by_id(session[:user_id])
      access_denied
      return false
    end
    def access_denied
      flash[:error] = 'Oops. You need to login before you can view that page.'
      redirect_to :log_in
    end
  end



You can prohibit unauthenticated users from using a specific action or controller by passing the symbol for the login_required method into before_filter. Here's how to protect the my_account action defined in app/controllers/user_controller.rb:

  class UserController < ApplicationController
    before_filter :login_required, :only => :my_account
......
   



Step 12) Next, try to add a restriction to your cookbook application like unauthenticated users are not allow to add a new recipe or delete a recipe.

Callbacks

Callbacks are methods that can be called before and/or after our named methods such as 'index' or 'show' are run in the controllers. Eighteen of the twenty callback methods are paired to be before and after our named methods. With the other two, one is available after instances are initialised, and the other is run after a finder method has been called.

Step 13) We'll add a callback to the app/model/recipe.rb file, as callbacks always go into the app/model directory. Add this code below to the recipe.rb file. This code will add a record to the development.log file under cookbook/log telling us that a recipe was added.

   after_create do |recipe|
      logger.info "recipe created: #{recipe.title} #{recipe.description}"
end

This is ok as far as it goes for our application. In other callbacks, though, there is more potential if we wanted to clean up submissions before they were processed, or if we needed to log details afterwards.

Notice, how little actual (were there any?) SQL we wrote for our database driven site.