Authentication with Devise in Rails

Build Blog with Ruby on Rails (Part 6)
Luan Nguyen
Luan Nguyen
Sep 20, 2020 · 7 min read

Photo by @hudsoncrafted on Unsplash


What'll we learn:

  • Introduction to Devise
  • Install Devise to Rails application
  • Create User model by Devise
  • Usage Devise for Authentication:
    • Create new User account
    • Log in - Log out
    • Reset password
  • Using Authentication to Manage Posts
  • Add the Post Details page

This is part 6 in Project: Build Blog with Ruby on Rails

Introduction

Devise is a flexible authentication solution for Rails, it supports to common functionality such as: create new account, sign in, sign up, reset password ...

It’s composed of 10 modules:
  • Database Authenticatable: hashes and stores a password in the database to validate the authenticity of a user while signing in. The authentication can be done both through POST requests or HTTP Basic Authentication.
  • Omniauthable: adds OmniAuth support.
  • Confirmable: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.
  • Recoverable: resets the user password and sends reset instructions.
  • Registerable: handles signing up users through a registration process, also allowing them to edit and destroy their account.
  • Rememberable: manages generating and clearing a token for remembering the user from a saved cookie.
  • Trackable: tracks sign in count, timestamps and IP address.
  • Timeoutable: expires sessions that have not been active in a specified period of time.
  • Validatable: provides validations of email and password. It’s optional and can be customized, so you’re able to define your own validations.
  • Lockable: locks an account after a specified number of failed sign-in attempts. Can unlock via email or after a specified time period.

Installing 

Add devise gem
Add this to your Gemfile:
gem 'devise', '~> 4.7'

then run bundle to install the gem:
bundle install

Next, you need to run the generator:
rails generate devise:install

It’ll auto-generate 2 files:
  • config/initializers/devise.rb
  • config/locales/devise.en.yml

Create User model by Devise

rails generate devise User

Output:
invoke active_record
  create db/migrate/YYYYMMDDHHMMSS_devise_create_users.rb
  create app/models/user.rb
invoke test_unit
  create test/models/user_test.rb
  create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :users

After running the command, it generates:
  • A migration to create User table: YYYYMMDDHHMMSS_devise_create_users.rb
  • User model: app/models/user.rb
  • Add routes for user and test files.
Open User model (app/models/user.rb), you can see default devise modules:
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

For this demo, we use additional 2 modules:
  • confirmable – Users will have to confirm their e-mails after registration before being allowed to sign in.
  • trackable - tracks sign in count, timestamps and IP address.
Modify User model like this:
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :validatable, :confirmable, :trackable

Besides, you have to edit the migration file (db/migrate/YYYYMMDDHHMMSS_devise_create_users.rb)
Uncomment the following lines like this:
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      [...]

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      [...]
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

Then run the migration to create User table
rails db:migrate

View details User table on rails console:
class User < ApplicationRecord {
                        :id => :integer,
                     :email => :string,
        :encrypted_password => :string,
      :reset_password_token => :string,
    :reset_password_sent_at => :datetime,
       :remember_created_at => :datetime,
             :sign_in_count => :integer,
        :current_sign_in_at => :datetime,
           :last_sign_in_at => :datetime,
        :current_sign_in_ip => :inet,
           :last_sign_in_ip => :inet,
        :confirmation_token => :string,
              :confirmed_at => :datetime,
      :confirmation_sent_at => :datetime,
         :unconfirmed_email => :string,
                :created_at => :datetime,
                :updated_at => :datetime
}

Tips: I use awesome_print gem to view that on rails console

Configure Devise

Initialize devise: config/initializers/devise.rb 
Devise.setup do |config|
  [ ...]
  # ==> Mailer Configuration
  # Configure the e-mail address which will be shown in Devise::Mailer,
  # note that it will be overwritten if you use your own mailer class
  # with default "from" parameter.
  config.mailer_sender = '[email protected]'

  [...] # More setting
end

Set up the default URL options for the Devise Mailer
In development environment: (config/environments/development.rb)
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

In production environment, host should be set to the actual host of your application.

You should restart your application after changing Devise’s configuration options (this includes stopping spring). Otherwise, you will run into strange errors, for example, users being unable to login and route helpers being undefined.

Usage Devise for Authentication

Devise supports some helpers to use inside your controllers and views.
To verify if a user is signed in, use the helper:
user_signed_in?

For the current signed-in user, use this helper:
current_user

Add Sign_up/ Log_in / Log_out links
You can add HTML code below to your view header of application:
# app/views/layouts/application.html.erb
<% if user_signed_in? %>
  <li class="nav-item">
    <%= link_to "Log out", destroy_user_session_path, method: :delete, class: "nav-link text-nowrap" %>
  </li>
<% else %>
  <li class="nav-item">
    <%= link_to "Log in", new_user_session_path, class: "nav-link text-nowrap" %>
  </li>
  <li class="nav-item">
    <%= link_to "Sign up", new_user_registration_path, class: "nav-link text-nowrap" %>
  </li>
<% end %>

  • destroy_user_session_path: Log out path, method :delete is default HTTP method used to sign out a User
  • new_user_session_path: Log in path.
  • new_user_registration_path: Create new user path

Sign Up: Create a new User account by click to Sign_up link, you'll see the view like that:
Sign up form

Flow Sign up:
  • Enter email, password and password confirmation => Click to Sign up button
  • User'll receive a confirmation email.
  • Click to confirmation link to verify the account.
  • Done, ready to Log in.

To read the confirmation email easy, I recommend installing a gem, called Letter Opener - which help we preview email in the default browser instead of sending it.

Install letter_opener gem:

Add the gem to your development environment and run the bundle command to install it.
group :development do
  [...]
  gem 'letter_opener'
end

Then set the delivery method in config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true

Restart server! and start to create a new User account.

An example of a confirmation-instruction-email on browser after using letter_opener:
Confirmation instruction Email


Log in: Click to Log_in link, you’ll see the form like that:
Log in form


Authentication for Post

We want to add the authentication functions for managing Posts (CRUD Post) for User. That means, only signed-in users are who can manage Posts.

To set up user authentication for a controller, just add this before_action:
before_action :authenticate_user!

class PostsController < ApplicationController
  [...]
  before_action :authenticate_user!
  [...]
end

When user NOT sign-in yet, and click to 'Manage Posts', the application auto-redirect user to the Log-in page. You can view log in the console like this:
Started GET "/posts" for ::1 at 2020-MM-DD 13:37:35 +0700
Processing by PostsController#index as HTML
Completed 401 Unauthorized in 2ms (ActiveRecord: 0.0ms | Allocations: 349)

Now, to manage CRUD Posts, you have to already signed in to system.

Add Post Details page

Create Blog controller with show action:
rails generate controller Blogs show
Output logs:
Running via Spring preloader in process 23109
      create  app/controllers/blogs_controller.rb
       route  get 'blogs/show'
      invoke  erb
      create    app/views/blogs
      create    app/views/blogs/show.html.erb
      invoke  test_unit
      create    test/controllers/blogs_controller_test.rb
      invoke  helper
      create    app/helpers/blogs_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/blogs.scss

Update Rails routes: (/blogs/:id)
Rails.application.routes.draw do
  [...]
  resources :blogs, only: [:show]
end


Update 'View details' link in Homepage:
From
<a href="#" class="btn btn-primary">View details</a>
To
<%= link_to 'View details', blog_path(post), class:"btn btn-primary" %>

In the Blog controller:
class BlogsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end
end

UI of the Post Detail page:
Post Details page


In View (app/views/blogs/show.html.erb)
<article class="blog-page">
  <div class="blog">
    <h2 class="title">
      <%= @post.title %>
    </h2>
    <div class="content">
      <%= @post.content %>
    </div>
  </div>
</article>

CSS of Post Details page:
We add 2 CSS files: 
  • blogs.scss
  • trix_editor.scss (optional): Override CSS of some elements in Trix content:
blogs.scss: 
.blog-page{
  max-width: 880px;
  margin: 0 auto;
  padding-top: 10px;

  .blog {
    padding: 40px 70px;
    img{
      border-radius: 8px;
    }
    .title {
      font-size: 34px;
      font-weight: bold;
      padding-bottom: 30px;
    }
  }
}

trix_editor.css:
.trix-content{
  font-size: 18px;
  line-height: 34px;
  h1{
    font-size: 29px;
    font-weight: 800;
    line-height: 28px;
    margin-bottom: 0.5em;
  }
  ol, ul {
    margin-top: 0;
    margin-bottom: 1em;
    padding-left: 30px;
    > li {
      margin-left: 0.5em;
      margin-bottom: 0.25em;
    }
  }
  pre{
    padding: 15px 20px;
    margin-top: 15px;
  }
  blockquote {
    color: rgba(0, 0, 0, 0.54);
    margin: 20px;
    padding-left: 20px;
  }
  strong{
    font-weight: 800;
  }
  .attachment__caption{
    padding-top: 10px;
  }
}

Import trix_editor.scss and blogs.scss to application.scss (import at the bottom of file)
[...]
@import "trix_editor";
@import "blogs";

Yey done!

Conclusion

In this article, I already guide you about the Devise gem and how to use Devise to add authentication functions as create a new User account, Login, Logout in Rails application.
Beside, I help you step by step add the Post details page to Blog application.