Ruby on Rails

How To Build Your Own Authentication in Rails 5

Building your own authentication in a Ruby on Rails application is easy using existing components in Rails. You could use existing gems like Devise, which are awesome, but in some cases they do too much, and are hard to work with if you want to do any modifications to conventional login behaviors. Rails already has built in authentication called has_secure_password, which is a module for Active Model classes that will take care of most of the hard work our authentication will require.

Setup

To demonstrate this, we'll create a new Ruby on Rails application:

$ rails new sample_app
$ cd sample_app

In the the Gemfile, uncomment or add the following gem which handles encryption:

gem 'bcrypt', '~> 3.1.7'

Then run bundle:

$ bundle

We will then create a controller to represent our application to a logged-in user. We'll generate these base files and edit them later:

$ rails generate controller Home index

Users

Users are who we are authenticating. To login to our application, they will need an email and password, but for has_secure_password, the only requirement on the User model is to have a password_digest field. Additional validations will be added to the class:

  • Password must be present on creation
  • Password length should be less than or equal to 72 bytes
  • Confirmation of password (using a password_confirmation attribute) (OPTIONAL)

Let's create the model by running the Rails scaffold generator, which will additionally add the forms, and views we'll use.

$ rails generate scaffold User email:uniq password:digest
$ rake db:migrate

The password:digest will add has_secure_password to our model, as well as add the extra attributes for password_digest and password_confirmation where needed in our forms and views.

In user.rb we'll add validations so that emails are unique (although we did add unique to our database, so an error will be raised regardless), but this will raise an error for our form and inform the user that their email is not unique:

class User < ApplicationRecord
  has_secure_password
  validates :email, uniqueness: true
end

We can also see the automatically generated views/users/new.html.erb (from our scaffold command above) our new User form:

<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>

  <div class="field">
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

  <div class="field">
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Password confirmation is optional. We have included password_confirmation in our form, which will be used and validated for, but if we were to exclude it, then no confirmation would be needed.

Sessions

We now create the Sessions controller and views to handle sessions.

$ rails generate controller sessions new create destroy

For logins, we need to the edit the new session login form app/views/sessions/new.html.erb:

<p><%= alert %></p>

<h1>Login</h1>
<%= form_tag sessions_path do |form| %>
  <div class=”field”>
    <%= label_tag :email %>
    <%= text_field_tag :email %>
  </div>
  <div class=”field”>
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>
  <div class=”actions”>
    <%= submit_tag %>
  </div>
<% end %>

<p>
  Or,
  <%= link_to "Sign Up", signup_path %>
</p>

We need to update our Sessions controller sessions_controller.rb:

class SessionsController < ApplicationController
  def new
  end
  def create
    user = User.find_by(email: params[:email])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notice: "You are logged in."
    else
      flash.now[:alert] = "Your email or password is invalid"
      render :new
    end
  end
  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: "You are logged out!"
  end
end

Routes

We need to set the routes for handling logins and logouts:

Rails.application.routes.draw do
  root to: "home#index"

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

  get "signup", to: "users#new", as: "signup"
  get "login", to: "sessions#new", as: "login"
  get "logout", to: "sessions#destroy", as: "logout"
end

Application Controller

Add the following code to the application_controller.rb to handle the current_user as well as the protective call to trigger the check for a current_user through the application. By using the before_action, we can be assured that the entire application is now covered by authentication.

class ApplicationController < ActionController::Base
  helper_method :current_user
  before_action :authenticate_user!

  def authenticate_user!
    unless current_user
      redirect_to login_path, notice: 'Please login'
    end
  end

  def current_user
    if session[:user_id]
      @current_user ||= User.find(session[:user_id])
    else
      @current_user = nil
    end
  end
end

For some controllers and specific views, we need to exclude them from authenticate_user!, which we can do in the individual controllers, which will be necessary in our Sessions Controller:

class SessionsController < ApplicationController
  skip_before_action :authenticate_user!
  ...
end

We can also exclude specific actions, which we need to do in our Users controller:

class UsersController < ApplicationController
  skip_before_action :authenticate_user!, only: [:new, :create]
  ...
end

This way, no authentication is done for user registration, but you need to be a user to view the index, show, and destroy actions.

Final Steps

The last thing we'll do is customize our Home index page to reflect that we've logged in and provide a link to logout:

<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

<% if current_user %>
  Logged in as <%= current_user.email %>.
  <%= link_to "Log Out", logout_path %>
<% else %>
  <%= link_to "Log In", login_path %> or
  <%= link_to "Sign Up", signup_path %> 

<% end %>
<p><%= notice %></p>

To test it out, start the Rails server:

$ rails server

Then, in the browser, go to the root path localhost:3000 and it should redirect to localhost:3000/login. You will need to register, so click sign up. Once registered, you should be able login.


Carson R Cole