header image

Authentication and Authorization in Rails Tutorial

   Back to list

You create an online web application and you want to define its access policy for the resources which you wish to protect? Or maybe you want to verify the identity of users visiting your website? In this tutorial, I will show you how simple it is to authenticate and authorizate your application using the popular rails gems: Devise, CanCanCan, and Rolify.

What is authentication and authorization?

Authentication is a confirmation of user identity, while authorization determines whether you can access a particular resource.

What is Devise?

Devise is a flexible authentication solution for Rails. It is composed of 10 modules. For example, one module called Trackable, tracks sign in counts, timestamps, and locates IP addresses. Creating a user authentication system is piece of cake when using Devise.

What is Rolify and CanCanCan?

Rolify is Roles library which supporting scope on resource object without any authorization enforcement. CanCanCan is an authorization library which restricts what resources a given user is allowed to access. All permissions are defined in a single location (the Ability class).

Step 1. Create a new Rails application

I used 4.2.6 version of Rails and SQLite as a database. Let’s skip a test and create a new Rails application.

$ rails new shop --skip-test-unit
$ cd shop
$ rake db:create

Step 2. Add Bootstrap and styles

Let’s add a ‘bootstrap-sass’ gem to our Gemfile. After cleanups and adding this gem, your Gemfile should look like this:

source 'https://rubygems.org'

gem 'rails', '4.2.6'
gem 'sqlite3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'bootstrap-sass', '~> 3.3.6'

group :development, :test do
 gem 'pry-rails'
end

group :development do
 gem 'web-console', '~> 2.0'
 gem 'spring'
end

Then bundle everything:

$ bundle install

Now let’s add some styles to our application. First, rename the application.css to the application.scss under the app/assets/stylesheets – in order to use imports. Now add these lines after the manifest:

@import "bootstrap-sprockets";
@import "bootstrap";

#main-container {
  position: relative;
  padding-top: 50px;
  padding-bottom: 50px;
}

.devise-container {
  width: 345px;
  padding-left: 15px;
}

.product-block {
  width: 750px;
}

.btn {
  text-decoration: none;
}

body {
  background: #f2f2f2;
}

th {
  background-color: #333333;
  color: white;
}

td {
  background-color: #808080;
  color: white;
}

Secondly, add after 15th list into assets/javascript/application.js file, this line:

//= require bootstrap-sprockets

Step 3. Edit application.html.erb

In this step, replace content of your views/layouts/application.html.erb file with this:

<!DOCTYPE html>
<html>
<head>
  <title>Shop</title>
  <%= stylesheet_link_tag	'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
  <nav class="navbar navbar-inverse navbar-top navbar-fixed-top">
    <div class="container-fluid">
      <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="/">Rails Tutorial</a>
   	</div>
      </div>
    </div>
  </nav>
  <div id="main-container" class="container">
    <%= yield %>
  </div>
</body>
</html>

Step 4. Add flash message

Now add two methods(bootstrap_classs and flash_messages) to app/assets/stylesheets/ApplicationHelper.rb which will be used to display messages.

application_helper.rb

Step 5. Add welcome page

First, create dashboard_controller.rb file under app/controllers folder. Add to this file:

class DashboardController < ApplicationController
  def index
  end
end

Next, create a dashboard folder under the app/views and create  the index.html.erb file. Let’s make this file looks like this:
index.html.erb

Now you have to open the routes.rb under the config folder. Clear file and add:

Rails.application.routes.draw do
  root 'dashboard#index'
end

Step 6. Start an application

Let’s start our application.
Run this command in your terminal:

$ rails s

Now go to http://localhost:3000 in your browser.
Your application should look like:

 

step6

Step 7. Create a product scaffold

Rails scaffolding is a quick way to generate some of the major pieces of an application.
Let’s create a model, controller, and views for a product in a single operation.

$ rails generate scaffold Product name:string price:float
$ rake db:migrate

We created a Product which contains name and price.
Remove from app/views/products the below files, as we don’t need them:

_product.json.jbuilder
index.json.jbuilder
show.json.jbuilder

Step 8. Add bootstrap to product files

Now, let’s modify files in app/views/products folder.
form.html.erb
edit.html.erb
new.html.erb
show.html.erb

index.html.erb:

<%= flash_messages %>
<h2>Products List</h2>
<div class="product-block">
  <table class="table table-striped table-bordered table-hover">
    <thead>
      <tr>
        <th class="col-md-2">Name</th>
    	<th class="col-md-2">Price</th>
    	<th class="col-md-4">Options</th>
      </tr>
    </thead>
    <tbody>
      <% @products.each do |product| %>
        <tr>
      	  <td class="col-md-2"><%= product.name %></td>
      	  <td class="col-md-2">$<%= product.price %></td>
      	  <td class="col-md-4">
            <%= link_to 'Show', product_path(product), class: "btn btn-info" %>
            <%= link_to 'Edit', edit_product_path(product), class: "btn  btn-warning" %>
            <%= link_to 'Destroy', product_path(product), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-danger" %>
      	  </td>
    	</tr>
      <% end %>
    </tbody>
  </table>
</div>
<br>
<%= link_to 'New Product', new_product_path, class: "btn btn-primary" %>

Let’s also clean app/controllers/products_controller.rb to look like this:

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def edit
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product, notice: 'Product was successfully created.'
    else
      render :new
    end
  end

  def update
    if @product.update(product_params)
      redirect_to @product, notice: 'Product was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @product.destroy
    redirect_to products_url, notice: 'Product was successfully destroyed.'
  end

  private
  def set_product
    @product = Product.find(params[:id])
  end

  def product_params
    params.require(:product).permit(:name, :price)
  end
end

And remove from the assets/stylesheets/scaffolds.scss file, this piece of code:

a {
  color: #000;

  &:visited {
    color: #666;
  }

  &:hover {
    color: #fff;
    background-color: #000;
  }
}

 

Step 9. Add products links to navigation bar

After adding products, we can add products links to the navigation bar. Add this piece of code to app/views/layouts/application.html.erb:

...21
<div id="navbar" class="navbar-collapse collapse">
  <ul class="nav navbar-nav">
    <li class="dropdown">
      <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Products <span class="caret"></span></a>
      <ul class="dropdown-menu">
        <li><%= link_to "List", products_path %></li>
        <li><%= link_to "New", new_product_path %></li>
      </ul>
    </li>
  </ul>
</div>
...33

Refresh your browser and go to Products->New, you should see:

step9

Step 10. Add authentication and authorization gems

Let’s move on to the most important thing in this article – authentication and authorization.
First, add these authentication and authorization gems to our Gemfile:

...12
gem 'devise'
gem 'cancancan', '~> 1.10'
gem 'rolify'
...16

Then bundle everything:

$ bundle install

Step 11. Add Devise to the project

We have added Devise to Gemfile, now let’s generate Devise files:

$ rails generate devise:install

After running this command, some instructions will be displayed on your console.
We only have to do step 1 and 4.
In the first step, ensure you have defined default url options in your environments files. Go to config/environments/development.rb file and add this line:

...1
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
...3

In the fourth step, we will add Devise views to our app by running:

$ rails g devise:views

Finally, create a User model by running:

$ rails generate devise User
$ rake db:migrate

Restart your app.

Step 12. Check Devise files

Let’s check three of the most important things that were added in previous step.

1) The new model User was created, and it contains the following Devise modules:

devise :database_authenticatable, :registerable,
     	:recoverable, :rememberable, :trackable, :validatable

2)In the config/routes.rb file, you can see that the new line was added:

devise_for :users

If we run the command:

$ rake routes | grep users

We see the routes that were created for users:

step12

3) All Devise views are located in views/devise folder.

Step 13. Add Bootstrap to Devise

Now add bootstrap to Devise views.
Modify files:
new.html.erb
links.html.erb

devise/registrations/edit.html.erb:

<h2>Edit <%= resource_name.to_s.humanize %></h2>
  <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
    <%= devise_error_messages! %>
    <div class="row">
      <div class="form-group col-md-4">
        <%= f.label :email %><br />
        <%= f.email_field :email, autofocus: true, class: "form-control" %>
      </div>
    </div>
    <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
      <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
    <% end %>
    <div class="row">
      <div class="form-group col-md-4">
        <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
        <%= f.password_field :password, autocomplete: "off", class: "form-control" %>
      </div>
    </div>
    <div class="row">
      <div class="form-group col-md-4">
        <%= f.label :password_confirmation %><br />
        <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
      </div>
    </div>
    <div class="row">
      <div class="form-group col-md-4">
        <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
        <%= f.password_field :current_password, autocomplete: "off", class: "form-control" %>
      </div>
    </div>
    <div class="actions">
      <%= f.submit "Update", class: "btn btn-primary" %>
    </div>
  <% end %>
<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: "btn btn-danger" %></p>

<%= link_to "Back", :back, class: "btn btn-default" %>

devise/registrations/new.html.erb:

<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>
<div class="row">
  <div class="form-group col-md-4">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, class: "form-control" %>
  </div>
</div>
<div class="row">
  <div class="form-group col-md-4">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "off", class: "form-control" %>
  </div>
</div>
<div class="row">
  <div class="form-group col-md-4">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
  </div>
</div>
  <div class="actions">
    <%= f.submit "Sign up", class: "btn btn-lg btn-primary" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

Step 14. Add users links to navigation bar

Let’s add user links to the navigation bar. Add this piece of code to app/views/layouts/application.html.erb:

...31
<ul class="nav navbar-nav navbar-right">
  <li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Profile <span class="caret"></span></a>
    <ul class="dropdown-menu">
      <li><%= link_to "Edit profile", edit_user_registration_path %></li>
      <li><%= link_to "Sign up", new_user_registration_path %></li>
      <li><%= link_to "Login", new_user_session_path %></li>
      <li role="separator" class="divider"></li>
      <li><%= link_to "Logout", destroy_user_session_path, method: :delete %></li>
    </ul>
  </li>
</ul>
..44

Reload your browser.

Step 15. Hide links from users that are not logged in

In the top right corner of the navigation bar, we have the Profile tab. When you expand it, you will see more tabs:

step15

Thanks to Devise, we can restrict which tab will be visible for logged and not logged in users. To verify if a user is signed in, we will use the following Devise helper:

user_signed_in?

Let’s modify the app/views/layouts/application.html.erb file:

...35
<% if user_signed_in? %>
  <li><%= link_to "Edit profile", edit_user_registration_path %></li>
<% else %>
  <li><%= link_to "Sign up", new_user_registration_path %></li>
  <li><%= link_to "Login", new_user_session_path %></li>
<% end %>
<% if user_signed_in? %>
  <li role="separator" class="divider"></li>
  <li><%= link_to "Logout", destroy_user_session_path, method: :delete %></li>
<% end %>
...46

Now, let’s reload your browser. After that, you will see that when you expand the Profile tab, only two tabs will be visible (Sign up and Login). It’s because you didn’t log into the application.

To do that, you have to click the Sign up tab, fill out the registration form, and submit it.

step15-signin

If you fill in the form with correct data, you will be logged into the application and will be redirected to the welcome page. Now when you expand the Profile tab, you will see the Edit Profile and Logout tabs instead of the Sign up and Login tabs.

Thanks to Devise, you can now sign in, log in, edit your account, and log out.

Step 16. Restrict access to products

Right now, even users that are not logged in can see, add, edit and remove products. Let’s make sure that only logged in users can add, edit and remove products. We can do this by adding to app/controllers/products_controller.rb this line:

...2
before_action :authenticate_user!, except: [:index, :show]
...4

After adding this line of code, reload the browser and check if not logged in user can for example add a new product. If you added the code right, users that are not logged in will will be redirected to the Log in panel and receive the message: “You need to sign in or sign up before continuing.”

step15-access

Step 17. Add a new field to the user

In this step, we will add a username field to the User. First, we have to generate a new migration:

$ rails g migration add_username_to_users username:string
$ rake db:migrate

Next, add username to the app/views/registrations/edit.html.erb and app/views/registrations/new.html.erb files:

new.html.erb
...10
<div class="row">
  <div class="form-group col-md-4">
	<%= f.label :username %><br />
	<%= f.text_field :username, autofocus: true, class: "form-control" %>
  </div>
</div>
...17

edit.html.erb
...9
<div class="row">
  <div class="form-group col-md-4">
    <%= f.label :username %><br />
    <%= f.text_field :username, autofocus: true, class: "form-control" %>
  </div>
</div>
...16

After that, add to your app/controllers/application_controller.rb file, the following lines:

...2
before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :email, :password])
    devise_parameter_sanitizer.permit(:account_update, keys: [:username, :email, :password, :current_password])
  end 
...11

We have now permited additional parameter username for Devise sign_up and account_update actions.
If you did everything correctly, you can now create a new user containing username.
Reload your browser and go to Sign Up. You should see that the new input called Username is displayed in Sign Up form.

step17

Step 18. Add user to product

Let’s say that we want to know who the product was created by. To achieve that, first we have to add user to the product. Run this comment in your terminal:

$ rails g migration AddUserToProducts user:references
$ rake db:migrate

After that, modify your app/models/product.rb file, to look like this:

class Product < ActiveRecord::Base
  belongs_to :user
end

and app/models/user.rb file, to look like this:

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
     	:recoverable, :rememberable, :trackable, :validatable
  has_many :products, dependent: :destroy
end

Then using the Devise helper method current_user, which returns a current signed-in user, we can add current signed-in user to product. Let’s modify the create action in your app/controllers/products_controller.rb file to look like:

def create
  @product = Product.new(product_params)
  @product.user = current_user
  if @product.save
    redirect_to @product, notice: 'Product was successfully created.'
  else
    render :new
  end
end

Finally, modify app/views/products/index.html.erb file by adding the Creator column to the table:

...7
 <th class="col-md-2">Price</th>
 <th class="col-md-2">Creator</th>
 <th class="col-md-4">Options</th>
...11

...16
 <td class="col-md-2">$<%= product.price %></td>
 <td class="col-md-2"><%= product.user.try(:email) %></td>
 <td class="col-md-4">
...20

Now let’s log into the application and create a new product. After that, go to the product list. In the column Creator, you will see a current user email alongside of the product which you have just now created. For example, when the user [email protected] creates a product which contains the name Keyboard and a price of 120 we get:

step18

 

Step 19. Add Roles

In step 12, we added Rolify to Gemfile. In this step, I will show you how we can use this gem. Let’s say that we want to create two types of users in our application: an admin, a person which will have access to every action in our application, and a client, a person with limited access. To do that, first we have to create Role model and migrate the database:

$ rails g rolify Role User
$ rake db:migrate

Finally, remove from app/models/Role.rb file, this line:

optional => true

Step 20. Populating the Database with seeds.rb

Let’s clean our database by running:

$ rake db:reset db:migrate

Next, we add some products to the database and create two users, Nicole (admin) and Bruce (client).

Please modify the db/seeds.rb file to look like this:
seeds.rb

and after that, run:

$ rake db:seed

Reload the browser and go to the product list, where you should see something like this:

step20

Step 21. Add CanCanCan to product

In step 12, we added CanCanCan to Gemfile. Let’s now use this gem.
Create the Ability class from CanCanCan:

$ rails generate cancan:ability
$ rake db:migrate

The Ability class is a place were user permissions are defined. Let’s add two helper methods to app/models/user.rb file:

...5
def admin?
  has_role?(:admin)
end

def client?
  has_role?(:client)
end 
...13

We will use these methods to check which role users have.

Modify your app/models/ability.rb file to look like:

class Ability
  include CanCan::Ability
  def initialize(user)
    user ||= User.new
    if user.client?
      can :manage, Product, user_id: user.id
    elsif user.admin?
      can :manage
    else
      can :read, :all
    end
  end
end

We defined that the client can perform any action on his own products. The admin can perform any action on all products and the not logged in user can only read (look over) all products.

Next, we have to add this line of code to app/controllers/products_controller.rb file:

...1
  load_and_authorize_resource
...3

This method loads the resource into an instance variable and authorizes it automatically for every action in products_controller.rb. If the user is not able to perform the given action, the CanCan::AccessDenied exception is raised. Let’s customize this exception by catching it and modifying its behavior. Add this piece of code to app/controllers/application_controller.rb:

...3
rescue_from CanCan::AccessDenied do |exception|
  redirect_to root_url, alert: exception.message
end
...7

We catch this exception, set the error message to flash, and redirect to the home page.

Now if you perform an action which a user is not able to perform, for example: remove admin product by client, you should get:

step21

Conclusion

In 21 steps, we created an application which contains simple authentication and authorization.
You can find the source code of the application here: https://github.com/nopio/authentication-and-authorization-tutorial
If you have any questions, feel free to contact us!

Send this to a friend