As we saw in the previous lesson, Rails scaffolding limitations can be overcome by enhancing our scaffold-generated MVC bundles with custom routes, migrations, actions, views and partials.
In this section we will do away with scaffolding altogether and build a simple login and registration script from the ground up. By doing so we will gain a better understanding of the MVC interrelationships.
Upon completion of this tutorial, you should be able to:
We want to add to our Rails blogging application the following use cases:
This application is for demonstrative purposes only. The administrator sections will not be password protected.
If you require a login/registration system for a Rails project I’d recommend looking into some of the authentication plugins I list in the last section of these notes.
Working with our existing blogging application:
Generate our model and prepare the database:
rails generate model User email:string password:string
rake db:migrate
Generate a controller with six actions (and associated views):
rails generate controller login index register create validate_user show_all delete
If you don’t know the name of your actions up front you can generate an empty controller:
rails generate controller login
As you add actions to the controller you will also have to create their associate html/erb view files.
Within your /config/routes.rb file you will need to modify the routes generated by your new controller as follows:
get "login/register"
post "login/create"
post "login/validate_user"
get "login/show_all"
delete "login/delete"
match "/login" => 'login#index'
Here we have change both the create and validate_user routes to respond to post requests, and the delete routes to respond to a delete request.
We have also used a match to simplify the login index route.
All of our login views will share some common XHTML. To avoid repetition this shared code is found in the /views/layouts/application.html.erb file.
Each view will be embedded into this layout replacing the line that reads:
<%= yield %>
We will now add two coloured paragraph tags to this file. These lines will output the contents of a hash called flash. We’ll see later how we load text into this hash.
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: red"><%= flash[:error] %></p>
The entire application.html.erb layout file:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Login: <%= controller.action_name %></title>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: red"><%= flash[:error] %></p>
<%= yield %>
</body>
</html>
User registration will involve the register view and the create action.
The register view will include a form with text inputs for the user’s email address and desired password. When the user submits the form it will be POSTed to the create action.
The email address and password will then be available to the action inside of the following nested hash:
params[:user][:email]params[:user][:password]Whenever an HTML form POSTs to a controller action the form elements will be passed to the action inside a params hash.
<h1>Register An Account</h1>
<%= form_for :user, :url => { :action => 'create' } do |f| %>
<%= f.label :email %>
<p><%= f.text_field :email %></p>
<%= f.label :password %>
<p><%= f.password_field :password %></p>
<p><%= f.submit 'Create User' %></p>
<% end %>
<%= link_to 'Back to Login', :action => 'index' %>
The form_for helper creates an HTML form element for us:
<form action="/login/create" method="post">
Within the form_for block we can use the f variable to create our other form element. For example, the f.text_field helper used for the email address generates the following HTML:
<input id="user_email" name="user[email]" size="30" type="text" />
In the previous section I mentioned that the form information would be POSTed to the create action in the params hash like this:
params[:user][:email]params[:user][:password]Looking at the form_for, text_field and password_field methods used above, can you see were we define the structure of the POSTed params hash?
From login_controller.rb:
def create
@user = User.new( params[:user] )
if @user.save
flash[:notice] = 'New user was created!'
redirect_to( :action => 'index' )
else
flash[:error] = 'Error: New user creation failed!'
redirect_to( :action => 'register' )
end
end
The User.new method expects a hash argument with an :email and :password key (with associated values). The params[:user] hash POSTed to us by the register view can therefore be used to create our new user.
If we are able to save the @user object we redirect to the index action, otherwise we will redirect to the register action.
Notice that we are using a hash called flash to store notice and error messages for the user. The login layout fetches these messages and outputs them above our views. We need to use this special hash to pass these messages to the layout because all other variables will loose scope after a redirect. This is because HTTP is a stateless protocol.
Note: No register action is required and the create action does not have an associated view. This means you can delete the generated create view.
As an administrator we may want to list all the users who have registered accounts.
To do this we will query the model from our show_all action and display the returned array of registered users in our show_all view.
We will also add the ability to delete users by creating buttons that will POST the id of the user we wish to delete to the delete action. The id will then be available to the delete action in the params hash.
def show_all
@users = User.all
end
<ul>
<% @users.each do |user| %>
<li>
Email: <%= user.email %> Password: <%= user.password %>
<%= button_to 'Delete',
{ :action => 'delete', :id => user.id },
:confirm => 'Are you sure?',
:method => :delete %>
</li>
<% end %>
</ul>
There will be one delete button generated for every user we list. Notice that this button’s HTTP method is set to DELETE.
When one of these buttons POSTs the user’s id to the delete action this id will be available via the params hash:
params[:id]
def delete
user = User.find( params[:id] )
if !user.nil?
user.destroy
flash[:notice] = 'User was deleted.'
else
flash[:error] = 'Error: User not found and cannot be deleted.'
end
redirect_to( :action => 'show_all' )
end
Here we use the model to locate a user by searching for the id passed to us by the delete button. As long as the model locates a user we will delete this user from the database.
The delete action does not have a view of it’s own. It will always redirect back to the show_all action. You can therefore delete the generated delete view file.
Note that we are using the flash hash again to pass messages to the login layout after a redirect.
In order to login to our application a user must submit their user name and password via a form. This form will POST their credentials to the validate_user action which will attempt to find a user in the database that matches the POSTed email and password.
The email and password sent by the form will be available to the action via the params hash:
params[:email]params[:password]<h1>Login to Top Secret Site</h1>
<%= form_tag '/login/validate_user' do %>
<%= label_tag :email %>
<p><%= text_field_tag :email %></p>
<%= label_tag :password %>
<p><%= password_field_tag :password %></p>
<p><%= submit_tag 'Login' %></p>
<% end %>
<%= link_to 'Register An Account', :action => 'register' %>
Because we are not directly dealing with a model object we use the form_tag helper to create our form, (instead of using form_for).
It’s worthwhile exploring the HTML generated by this view to discover how the *_tag methods work. For example, password_field_tag 'password' generates:
<input id="password" name="password" type="password" />
Note: Whereas the form in our register view POSTed into the params[:user] hash (giving us params[:user][:email] and params[:user][:password]), this form will post directly into params:
params[:email]params[:password]This is because form_for and form_tag generate slightly different HTML forms. You may want to explore the two generated views to see the difference for yourself.
def validate_user
@user = User.where["email = :email and password = :password",params]).first
if @user.nil?
flash[:error] = 'Username / Password Incorrect'
redirect_to( :action => 'index' )
end
end
The validate_user view will only be loaded if the find command locates a user that matches the email and password POSTed by the index form.
If no user is found we are redirect back to the index view.
The user is located by using our User model’s find method to search for the :first row in the database that matches the :conditions I’ve specified. These conditions will be used to construct a WHERE clause for the SQL generated by the find method.
There are various ways to specify find conditions. In this case I’ve specified an array. The first element of the array is a string:
email = :email and password :password
Where :email and :password can be found in the second array element, the params hash.
If the validate_user action completes execution without redirecting to the index action we will be display the following view.
validate_user.html.erb:
<h1>Login Successful!</h1>
<p>
Welcome user <%= @user.email %> you have been validated.
</p>
In the login system we’ve just implemented the passwords of all our users are stored in plaintext in our database.
Many users use the same password on all sites. If our server gets hacked and our database is copied our users could be very angry indeed.
It’s probably best to encrypt our user passwords during the registration process before we store them in the database.
To secure the passwords stored in our database we will encrypt them using a one-way hash function.
A hash function (also know as a digest, fingerprint, or compression function) is an algorithm that takes a variable-length string as input and produces a fixed-length binary value (hash) as the output.
When used for cryptographic purposes the hashing process needs to be irreversible (one-way) so that a hacker with access to our database cannot convert the hashed passwords back into their plaintext strings.
We’ll be using the MD5 hash for our login system.
if (curious about the link between hashing algorithms and our favourite Ruby data structure the hash)
read the Hash table entry @ Wikipedia
else
programming is hard, let’s go shopping.
Currently when a user registers, we store their username and password in plaintext in our database. When logging in, a user supplies a username/password and we search our database for a row that contains the provided username/password. The user is considered authenticated if we find a matching row.
To secure our database we will hash the password provided during registration and store this hash in the database instead of the plaintext password. When logging in, we must now hash the user’s supplied password before we search our database for a matching user.
We’ll need to add some class methods to our User model to implement our secure password. In Ruby a class method is a method that belongs to a class which can be executed without instantiating an object of that class. For example, the ActiveRecord find method is a class method:
User.find(1)
Notice how we are calling find via the User class and not via an object of type User.
We can add class methods to a class by defining prefixing their method definition names with ’self.’.
To add a demo class method to the User class we would do the following:
class User < ActiveRecord::Base
def self.demo
puts "I am the demo class method of the User class."
end
end
This method could then be called from any controller action like this:
User.demo
The first method we require will take a plaintext string as an argument and return the MD5 hash for that string.
require 'digest/md5' # This module is needed for MD5 hashing.
class User < ActiveRecord::Base
protected
def self.encrypt_password plaintext
Digest::MD5.hexdigest( plaintext )
end
end
It’s also worth noting that although I call this ‘encryption’ this isn’t technically true. Encryption is a two-way process (encryption/decryption) while as we’ve seen, cryptographic hashing is one-way.
Currently the create action creates a new user like this:
@user = User.new( params[:user] )
Let’s change that line to:
@user = User.new_encrypted( params[:user] )
And create a new_encrypted class method for the User class:
def self.new_encrypted user
user[:password] = encrypt_password( user[:password] )
new( user )
end
Note that this new_encrypted method hashes our plaintext password (using the encrypt_password method) before we store the new user in the database.
The call to new is equivalent to calling User.new from an action.
Our validate_user action also needs to be updated. Instead of setting the @user variable by calling the find method, let’s do the following:
@user = User.find_encrypted(params)
Next we need to add a find_encrypted class method to the User class:
def self.find_encrypted user
user[:password] = encrypt_password( user[:password] )
find.where(["email = :email and password = :password",user]).first
end
Because our user passwords are stored as hashes in our database, we need to hash the user’s login password before we search the database. Again we use the encrypt_password class method that we implemented earlier to perform the hashing.
Even though MD5 is a fairly secure hashing algorithm there are services on the internet that allow you to crack MD5 hashed passwords of common words.
For example, here is a MD5 hash of a single word:
dc5d6e5c0ffd6d1cd249286ced098382
Try entering this hash into the GData MD5 cracking tool.
Oh noes! They be crack’n our hashes!
Hashing schemes can be made more secure using a technique called salting. This topic lies outside the scope of our course, but I’ve provided some resources on salting below.
We now have a system in place that allows user to register and log in. If you want to explore a more secure and feature-complete user authentication system you can follow the chapter 11 tutorial of our textbook.
The system implemented in chapter 11 uses salted SHA1 hashing for password storage. It also uses the Rails session mechanism to allow our server to “remember” logged in users.
Better yet, there are a number of pre-built authentication plugins provided by the Rails community.
I have provided links to a few of these below: