User Authentication

User Authentication is an important part of most web applications.

There are gems that can provide awesome authentication functionality out of the box, like Devise and Clearance.

However, it's also a good exercise in learning to implement our own authentication and understand the concepts for building out such a functionality.

And after you are familiar with the concepts, using any of those gems above would seem less intimidating.

In this guide, we'll build an extremely simple User Authentication:

  • User Sign Up (with Email/Password)
  • User Sign In/Out

The following will be out of scope:

  • User Sign Up/In with Social Media Accounts
  • Email Confirmation of Account
  • Reset Password
  • Remember Me
  • User Tracking
  • Account Lockdown (when failed sign in > X times)

1. User Model

Let's build our basic user model (completed as an exercise previously):

$> rails generate Model User name:string email:string

This would generate an empty user.rb and a migration for the users table. Run the migration:

$> rake db:migrate

Finally, we'll add basic validations to the User model:

class User < ActiveRecord::Base
  validates_presence_of :name, :email
  validates_format_of   :email, with: /@/
end

2. Uniqueness of Email

The User Authentication that we'll be building will user Email and Password as login credentials.

For this to happen, email has to be unique in the database, so that there's no ambiguity for user login.

To ensure uniqueness for the email column, we can do it at the code level or database level.

Let's do it on both levels to ensure that no one gets through the crack.

Firstly, for the database, we need to add an unique index:

$> rails generate migration add_index_to_users_email

Open up the generated migration file and update the contents:

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

Secondly, add the uniqueness at the code level, specifically, in the User model:

validates_uniqueness_of :email

Depending on the database that you are using, searching could be case sensitive or case insensitive:

PostgreSQL (Case Sensitive) MySQL (Case Insensitive)
User.find_by(email: '[email protected]') User A User A
User.find_by(email: '[email protected]') User B User A

From the example above, you can see the impact of case sensitivity on database queries.

Usually in User Authentication, we would prefer our queries to be case insensitive, so that [email protected] and [email protected] both returns the same user.

To do that, it's actually easier then to ensure that we downcase the email before saving to the database. At the same time, our uniqueness index (on the code level) should be case insensitive too.

Let's update our User model again, and you should end up with:

class User < ActiveRecord::Base
  validates_presence_of   :name, :email
  validates_format_of     :email, with: /@/
  validates_uniqueness_of :email, case_sensitive: false

  before_save { self.email = email.downcase }
end

3. Password Fields

How do we handle passwords?

It is important to remember that we NEVER STORE PASSWORD IN CLEAR in the database. They have to be encrypted.

Comparison of passwords are also performed on the encrypted versions, instead of the original password.

In this way, it's difficult to find out the original password for a user (even for developers).

For encryption to work, we'll use the bcrypt gem in Gemfile (find and uncomment) then run `bundle install:

gem 'bcrypt', '~> 3.1.7' #

Rails provides a convenience method for use to do this: has_secure_password and we can use it in User model:

class User < ActiveRecord::Base
  has_secure_password

  # other code ...
end

Referencing the Guide, we'll need to add a new column password_digest to users:

$> rails generate migration add_password_digest_to_users password_digest:string

Run the migration:

$> rake db:migrate

4. User Sign Up

"User Sign Up" is equivalent to "Creation of User", except that this is open to public.

(You might have completed this from the earlier exercise)

Let's first add the routes in config/routes.rb:

resources :users

Next, let's create a UsersController:

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

  def create
    @user = User.new(params.require(:user).permit(:name, :email, :password, :password_confirmation))

    if @user.save
      redirect_to @user
    else
      render :new
    end
  end

  def show
    @user = User.find(params[:id])
  end
end

By now, you should be familiar with how a Rails controller and actions work..

We have create 3 actions (or methods), where new will render app/views/users/new.html.erb and display a form for users to sign up with "Name", "Email", "Password" and "Password Confirmation".

create will do the actual creation of a User in the database with the 4 whitelisted attributes, and redirect to show on success, or re-render new on failure.

show will display the details of a user for now. Later, it should display the grams of this user.

Then, we'll go ahead and create our views, starting with new.html.erb:

<% if="" @user.errors.any?="" %="">
    <% @user.errors.full_messages.each="" do="" |msg|="" %="">
  • <%= msg="" %="">
  • <% end="" %="">
<% end="" %=""> <%= form_for(@user)="" do="" |f|="" %="">
<%= f.label="" :name="" %=""> <%= f.text_field="" :name,="" class:="" 'form-control'="" %="">
<%= f.label="" :email="" %=""> <%= f.text_field="" :email,="" class:="" 'form-control'="" %="">
<%= f.label="" :password="" %=""> <%= f.password_field="" :password,="" class:="" 'form-control'="" %="">
<%= f.label="" :password_confirmation="" %=""> <%= f.password_field="" :password_confirmation,="" class:="" 'form-control'="" %="">
<%= f.submit="" 'Sign="" Up',="" class:="" 'btn="" btn-success'="" %="">
<% end="" %="">

And for show, we can just do something as simple as (if you already have something, just leave it):

<%= @user.inspect="" %="">

Now, any user can sign up!

(But we lack the links to go to the sign up page in the navigation bar.)

5. Sign In, Sign Out

To manage "Sign In" and "Sign Out", we need a SessionsController which will manage the session for a user:

class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user && @user.authenticate(params[:session][:password])
      session[:user_id] = @user.id
      redirect_to root_path
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    session.delete(:user_id)
    @current_user = nil

    redirect_to root_path
  end
end

new will display a "Sign In" form which will have "Email" and "Password" as the form fields.

On submission of the "Sign In" form, it will POST to create which will attempt to login the user:

  • Find user by email (downcase, because we store all our emails as downcase)
  • Use the authenticate method provided by has_secure_password to compare encrypted passwords

If login is successful, the User's id will be saved to session which will be available to other requests.

Otherwise, it will re-render the "Sign In" form.

flash.now will allow Flash messages to be used for the current request (while flash will pass the message to the next request).

The destroy method is for logout where the session will be cleared and redirected to root_path.

The "Sign In" form at new.html.erb will be like this:

<%= form_for="" :session,="" url:="" sign_in_path="" do="" |f|="" %="">
<%= f.label="" :email="" %=""> <%= f.text_field="" :email,="" class:="" 'form-control'="" %="">
<%= f.label="" :password="" %=""> <%= f.password_field="" :password,="" class:="" 'form-control'="" %="">
<%= f.submit="" 'Sign="" In',="" class:="" 'btn="" btn-success'="" %="">
<% end="" %="">

Add these to config/routes.rb to make our SessionsController accessible:

get    'sign_in'   => 'sessions#new'
post   'sign_in'   => 'sessions#create'
delete 'sign_out'  => 'sessions#destroy'

Finally, let's add the "Sign Up", "Sign In" and "Sign Out" links to our layout layouts/application.html.erb, and also allow flash[:error] to be displayed:

<!DOCTYPE html>
<html>
<head>
  <title>Minigram</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
  <script src="//widget.cloudinary.com/global/all.js" type="text/javascript"></script>
</head>
<body>

<nav class="navbar">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="<%= root_path %>">minigram</a>
    </div>
    <div id="navbar" class="collapse navbar-collapse pull-right">
      <ul class="nav navbar-nav">



        <!-- !!!!!OVER HERE!!!!! --->
        <% if current_user %>
          <li><%= link_to 'My Minigram', user_path(current_user) %></li>
          <li><%= link_to 'Sign Out', sign_out_path, method: :delete %></li>
        <% else %>
          <li><%= link_to 'Sign Up', new_user_path %></li>
          <li><%= link_to 'Sign In', sign_in_path %></li>
        <% end %>
        <!-- !!!!!OVER HERE!!!!! --->



      </ul>
    </div>
  </div>
</nav>

<div class="container">
  <% if flash[:notice] %>
    <div class="alert alert-info"><%= flash[:notice] %></div>
  <% end %>



  <!-- !!!!!OVER HERE!!!!! -->
  <% if flash[:error] %>
    <div class="alert alert-danger"><%= flash[:error] %></div>
  <% end %>
  <!-- !!!!!OVER HERE!!!!! -->




  <%= yield %>
</div>

</body>
</html>

Notice that in the new code which we just added, we checked for if current_user.

This method current_user doesn't actually exist and we need to add it into ApplicationController:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  helper_method :current_user

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

Basically, it will find the User using the session[:user_id] which we stored when a user logins successfully.

The helper_method :current_user then exposes this method so that it can used in the views.

6. Associate User to Grams

A user can now "Sign Up", "Sign In" and "Sign Out", but we are still seeing all grams from the database.

Let's make it such that I can see a user's Grams if I browse to /users/winston.

To do that, we need to associate User and Grams, so that a User has_many Grams, and a Gram belongs_to a User.

Let's start with the migration:

$> rails generate migration add_user_id_to_grams user_id:integer:index

This will generate a migration that adds a new column user_id to the grams table, with index. Run migration:

$> rake db:migrate

Next, let's add our associations:

class User < ActiveRecord::Base
  has_many :grams

  # other code...
end
class Gram < ActiveRecord::Base
  belongs_to :users

  # other code...
end

Now, let's think about it. If we create a Gram right now, how do we know which User it belongs to? We don't!

Hence we need to make sure that we assign the User when a Gram is created:

def create
  @gram = Gram.new(params.require(:gram).permit(:title, :content, :country_code, :image, :image_cache))

  # !!!!! Associate a Gram to a User !!!!!
  @gram.user = current_user


  if @gram.save
    redirect_to @gram, notice: 'You have successfully grammed!'
  else
    render :new
  end
end

And when we access users/winston (which is the UsersController's show action), we need to display specifically the Grams for this user only:

def show
  @user  = User.find(params[:id])
  @grams = @user.grams.page(params[:page]).per(5)
end

Let's display the Grams in users/show.html.erb:

<% @grams.each="" do="" |gram|="" %="">
  
Me
<%= link_to="" time_ago_in_words(gram.created_at),="" gram_path(gram)="" %="">   <%= link_to="" icon('trash-o'),="" gram_path(gram),="" method:="" :delete,="" data:="" {="" confirm:="" 'Are="" you="" sure="" want="" to="" delete="" this="" Gram?'}="" %="">
<%= image_tag="" gram.image_url(:cropped)="" %="">
<% end="" %="">
<%= paginate="" @grams="" %="">

7. Protect Gram Creation

We only want users who have signed in to be able to create grams.

Let's first hide the new gram link in our grams/index.html.erb page:

<% if="" current_user="" %="">
  
<%= link_to="" icon('camera'),="" new_gram_path="" %="">
<% end="" %="">

That's great! But if a user already knows the link to the new Gram form, then he/she can still access it.

Hence we need to protect the new and create methods in GramsController from unauthenticated user too.

Let's make the updates to GramsController:

class GramsController < ApplicationController
  before_action :authenticate_user, only: [:new, :create]

  # other code...

  # at the bottom of the file
  private

    def authenticate_user
      redirect_to root_path unless current_user
    end
end

before_action will run before new or create is executed and it will check if current_user exists.

If not, then redirection to root_path occurs.

8. Author Can Delete Gram

At the same time, you can only delete your own gram and not others gram.

Let's first make sure that only gram authors can see the delete link in the views.

Starting with grams/index.html.erb:

<% if="" gram.user="=" current_user="" %="">
  <%= link_to="" icon('trash-o'),="" gram_path(gram),="" method:="" :delete="" %="">
<% end="" %="">

And with grams/show.html.erb:

<% if="" gram.user="=" current_user="" %="">
  <%= link_to="" icon('trash-o'),="" gram_path(@gram),="" method:="" :delete="" %="">
<% end="" %="">

In the GramsController, we need to protect the destroy method as well, and we just have to update the before_action to include destroy.

before_action :authenticate_user, only: [:new, :create, :destroy]

It is also a good practice to make sure that whenever we run the destroy method, the gram belongs to the current_user. Hence, we'll update our find method in the destroy method:

def destroy
  @gram = current_user.grams.find(params[:id])
  @gram.destroy

  redirect_to grams_path
end

Congrats. You have built a very solid authentication from scratch.