Note: This article has been updated to correct typos. Also, as this will be part of a series, Rails 5.2 will be required. However, this particular article should work just fine on older versions of Rails.

Introduction

One of the very first articles I ever published was a simple little piece called Authentication in Rails 3.1+. That article is still surprisingly useful to this day, even though Rails has changed drastically. I figure 5 years later it's time for a follow up. In this article I will show you how to build an authentication system from scratch...well almost.

You see, Rails actually performs a lot of the heavy lifting for us with the help of a tiny library known as bcrypt. We only need to perform a few minor steps in order to enable this functionality. These steps are outlined below, but first: A few definitions are in order for the new folks.

  • Password Hash A password hash is a secure way to represent the password without needing to know what the password is. For example, using a simple hashing algorithm, the password 'foo' would be equal to acbd18db4cc2f85cedef654fccc4a4d8. This random series of letters and numbers is not reversible by any standard means. This is different from encryption, where all data can be decrypted.

  • Password Salt - A password salt is a series of characters that is usually inserted at the beginning of the password in order to randomize the hash and make it it harder to guess. Password salts are used when storing passwords in order to make it harder to perform brute force attacks on our hashed passwords.

This article should work with minimal modifications on Rails 4 and up, but we use Rails 5 here. If you have problems with an earlier version of Rails, feel free to leave a comment below. Let's get started!

Rails Application Setup

First, we need to add the bcrypt gem to our Gemfile. The bcrypt library is used to hash and salt passwords in a secure way. Why do we need to do this? Storing the passwords in plain text is considered to be a bad security practice. If your website gets compromised and the database gets copied, the attackers now have the credentials of all of your users. In addition, encryption is considered to be a poor security practice because anyone with the encryption key can easily decrypt the entire list of passwords. You also shouldn't try and reinvent the wheel with your own way of storing passwords. Doing so without understanding all the nuances of security can leave you open to an attack. Instead, let's use an existing, proven library to take care of the dirty work for us.

Let's start by addding the bcrypt gem to our Gemfile now.

Gemfile:
gem 'bcrypt`

Now run bundle to install the gem.

Terminal:
bundle

There isn't anything else to do as far as setup goes at this point. Rails takes care of integrating bcrypt. I will now show you how to build out the rest of your application.

Creating Our User Model

First, you will need to create a model to store user information. The User model will contain a couple of different attributes. The email attribute will store a unique email address, and the password_digest attribute will store the hashed, salted password. Run the command below to create the User model.

Terminal:
rails g model User email password_digest

Great, now run rails db:migrate to migrate the database.

Terminal:
rails db:migrate

Now it's time to add some code to our model to support the authentication system. Open up the User model at app/models/user.rb and modify it so that it looks like the code listed below.

app/models/user.rb:
class User < ApplicationRecord
  has_secure_password
  validates :email, uniqueness: true, presence: true
end

What does has_secure_password do? It adds two additional properties to your user: password and password_confirmation. In addition, it adds a number of different validations (presence, length with a maximum of 72, and confirmation). Finally, it adds a method called authenticate which allows you to authenticate the user against the database. Let's move on.

Tip: Feel the need to create your own custom password validations? You can skip the generation of the default validations by passing validations: false to the has_secure_password method. This will tell Rails not to add ANY validations to the password and password confirmation fields though, so use with caution.

Controller Setup

In this example we will have a total of three different controllers. The first controller will be called dashboard. The dashboard controller and associated show view will display a simple welcome message and require a valid user. The second controller will be called sessions. The sessions controller will allow the user to log in and out of the site via the new, create, and destroy views. The third controller will be called users. The users controller will handle new user registration via the new and create methods. Run the commands below to create these controllers.

Terminal:
rails g controller dashboard show
rails g controller sessions new create destroy
rails g controller users new create

Now let's update our routes file. Open the config/routes.rb file and modify it so that it looks like the code below.

config/routes.rb:
Rails.application.routes.draw do
  root 'dashboard#show'

  get 'users/new'
  get 'login', to: 'sessions#new'
  get 'logout', to: 'sessions#destroy'

  resources :sessions, only: [:create]
  resources :users, only: [:new, :create]
end

Let's break this down. Line 2 sets the site root to the show view of our Dashboard controller. Lines 5 and 6 define custom /login and /logout urls that map to our sessions controller's new and destroy methods. Lines 7 and 8 set up a few other default routes that we will need. Let's move on.

Now I'll show you how to require a logged in user. First, open up app/controllers/application_controller.rb and modify it so that it looks like the code below.

app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
  before_action :require_valid_user!

  def current_user
    if !session[:user_id].blank?
      @user ||= User.find(session[:user_id])
    end
  end

  def require_valid_user!
    if current_user.nil?
      flash[:error] = 'You must be logged in to access that page!'
      redirect_to login_path
    end
  end
end

So what does the code above do? The current_user function at lines 4-8 returns the currently logged in user or nil if the user isn't logged in. The require_valid_user! function at lines 10-15 redirects the user to the login page if he/she is not logged in. Line 3 calls this function by default for every page in the example.

Now let's work on the sessions controller. The sessions controller, as mentioned earlier, contains methods that are used to log in/out of the example. Open up app/controllers/sessions_controller.rb and modify it so that it looks like the code listed below.

app/controllers/sessions_controller.rb:
class SessionsController < ApplicationController
  skip_before_action :require_valid_user!, except: [:destroy]

  def new
  end

  def create
    reset_session
    @user = User.find_by(email: session_params[:email])

    if @user && @user.authenticate(session_params[:password])
      session[:user_id] = @user.id
      flash[:success] = 'Welcome back!'
      redirect_to root_path
    else
      flash[:error] = 'Invalid email/password combination'
      redirect_to login_path
    end
  end

  def destroy
    reset_session
  end

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

Line 2 ensures that we aren't requiring the user to be logged in for the new and create methods.

The create method at Lines 7-19 handle the process of logging in:

  • Line 8 resets the user's session. This helps prevent session fixation and other attacks.
  • Line 9 finds the user by his/her email address.
  • Lines 11-18 checks if the user exists and they are allowed to authenticate. If not, an error is displayed and the user is redirected to the login page. If the user was successfully authenticated they are redirected to the dashboard page.

The destroy function at lines 21-23 resets the user's session, causing them to be logged out.

Now I'll show you how to handle new user registration. Open up app/controllers/users_controller.rb and modify it so that it looks like the code listed below.

app/controllers/users_controller.rb:
class UsersController < ApplicationController
  skip_before_action :require_valid_user!
  before_action :reset_session

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      flash[:success] =  'You have successfully created an account.  Please sign in to continue.'
      redirect_to login_path
    else
      render :new
    end
  end

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

So what's going on here? Line 2 ensures that our session is clean before we register. Line 3 of the users controller skips the code that requires the user to be authenticated. The create function at lines 10-19 handles new user registration. Once the user successfully creates an account, they are redirected to the login page. Pretty simple right? Let's move on.

View Setup

Now that our models and controllers are out of the way, we can work on our views. First, let's work on the application layout. We only need to make a small change. We will include the CSS file for the popular Bootstrap library in order to improve the look/feel of our example. Open up app/views/layouts/application.html.erb and modify it so that it looks like the code listed below.

app/views/application.html.erb:


  
    AuthenticationExample
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_link_tag 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css' %>
  

  
    
<%= yield %>

The only thing we do here is link to the Bootstrap 4.1.2 file on the Bootstrap CDN and wrap the rendering of our views in a div with a class of container. Let's move on.

Now I'll show you how to create the dashboard page that we will use for this example. Open up app/views/dashboard/show.html.erb and modify it so that it looks like the code listed below.

app/views/dashboard/show.html.erb:

Dashboard

Welcome to your dashboard.

<% if flash[:success] %>
<%= flash[:success] %>
<% end %> <%= link_to 'Click here to log out.', logout_path %>

The page is simple. It has a bit of text, renders the flash success message if it is populated, and has a link to log out.

Now I'll show you how to create the login/logout pages. First let's work on the login page. Open up app/views/sessions/new.html.erb and modify it so that it looks like the code listed below.

app/views/sessions/new.html.erb:

Authentication Required

Please log in to continue.

<%- if flash[:error] %>
<%= flash[:error] %>
<% elsif flash[:success] %>
<%= flash[:success] %>
<% end %> <%= form_for :session, url: sessions_path do |f| %>
<%= f.label :email %> <%= f.email_field :email, class: 'form-control', placeholder: 'Email' %>
<%= f.label :password %> <%= f.password_field :password, class: 'form-control', placeholder: 'Password' %>
<%= f.submit 'Log In', class: 'btn btn-primary' %>
<% end %>

New User? <%= link_to 'Click here to create an account.', new_user_path %>

Let's break this down:

  • Lines 4-8 display any flash messages that may be present.
  • Lines 10-22 defines a form that we will use to login:
    • The f.email_field function at line 13 creates a text box that has additional keyboard features on mobile devices. This includes an @ symbol on the main keyboard and some other features depending on your mobile device. Other than the custom keyboard, the email field behaves just like a regular text box.
    • The f.password_field function at line 17 creates a text box that masks the password so that it cannot be viewed on screen.
    • The f.submit function at line 20 submits the form.
    • Line 25 contains a link to our new user registration page.

Next, I'll show you how to create the logout page. Open up app/views/sessions/destroy.html.erb and modify it so that it looks like the code listed below.

app/views/sessions/destroy.html.erb:
<h1>Log Out</h1>
<p>You have successfully logged out.  <%= link_to 'Click here', login_path %> to log in again.</p>

This view is pretty simple. it contains a bit of text along with a link to log in again.

Now I'll show you how to create the new user registration page. Open up app/views/users/new.html.erb and modify it so that it looks like the code listed below.

app/views/users/new.html.erb:
<h1>Sign Up</h1>
<p>Create a new account using the form below.  All fields are required.</p>

<% if @user.errors.any? %>
  <div class='alert alert-danger'>
    <p>Please correct the following errors:</p>
    <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

<%= form_for @user do |f| %>
  <div class='form-group'>
    <%= f.label :email %>
    <%= f.email_field :email, class: 'form-control' %>
  </div>
  <div class='form-group'>
    <%= f.label :password %>
    <%= f.password_field :password, class: 'form-control' %>
  </div>
  <div class='form-group'>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>
  </div>
  <%= f.submit 'Register', class: 'btn btn-primary' %>
<% end %>

<p class='mt-2'>
  Already have an account? <%= link_to 'Click here to sign in.', login_path %>
</p>

So what's going on here? Lines 4-13 display any validation errors that may be present. At line 15, we create a form for the user. You'll notice we have an email field along with two different password fields here, one for password and one for password confirmation. Finally we have a "Register" button that submits the form. At the very bottom, we have a link to login that we can click if we already have an account.

That's it!

Conclusion

Building out our own authentication system is pretty simple. In future articles I will show you how to add more features. For now, start your Rails development server and try it out! Thanks for reading!

HEY YOU! Like what you see? Want to support RichOnRails.com? If you live in the US, make sure you hit up Amazon.com. and buy some stuff. If you don't live in the US or you don't want to buy anything, you can also support this site by purchasing a Pro subscription at: https://richonrails.com/pro. Performing these actions helps us pay for things like hosting and new content. In addition, we hope to eventually provide financial support for your favorite open source projects. So subscribe today!