Authentication from Scratch in Rails 5

Basic Rails 5 App

Create a new Rails 5 project.

rails new au5

Create an article model.

rails g model article name author content:text published_at:datetime

Migrate the database.

rails db:migrate

Create some sample data in seeds.rb.

superman = Article.create! name: "Superman", author: "Clark Kent", published_at: 1.weeks.ago, content: "Superman is a fictional comic book superhero appearing in publications by DC Comics, widely considered to be an American cultural icon. Created by American writer Jerry Siegel and Canadian-born American artist Joe Shuster in 1932 while both were living in Cleveland, Ohio, and sold to Detective Comics, Inc. (later DC Comics) in 1938, the character first appeared in Action Comics #1 (June 1938) and subsequently appeared in various radio serials, television programs, films, newspaper strips, and video games. (from Wikipedia)"
krypton = Article.create! name: "Krypton", author: "Clark Kent", published_at: 4.weeks.ago, content: "Krypton is a fictional planet in the DC Comics universe, and the native world of the super-heroes Superman and, in some tellings, Supergirl and Krypto the Superdog. Krypton has been portrayed consistently as having been destroyed just after Superman's flight from the planet, with exact details of its destruction varying by time period, writers and franchise. Kryptonians were the dominant people of Krypton. (from Wikipedia)"
batman = Article.create! name: "Batman & Robin", author: "Bruce Wayne", published_at: 2.weeks.ago, content: "Batman is a fictional character created by the artist Bob Kane and writer Bill Finger. A comic book superhero, Batman first appeared in Detective Comics #27 (May 1939), and since then has appeared primarily in publications by DC Comics. Originally referred to as The Bat-Man and still referred to at times as The Batman, he is additionally known as The Caped Crusader, The Dark Knight, and the World's Greatest Detective, among other titles. (from Wikipedia)"

Article.create! name: "Wonder Woman", author: "Diana of Themyscira", published_at: 6.weeks.ago, content: "Wonder Woman is a DC Comics superheroine created by William Moulton Marston. She first appeared in All Star Comics #8 (December 1941). The Wonder Woman title has been published by DC Comics almost continuously except for a brief hiatus in 1986. (from Wikipedia)"
Article.create! name: "Flash", author: "Bart Allen", published_at: 5.weeks.ago, content: "The Flash is a name shared by several fictional comic book superheroes from the DC Comics universe. Created by writer Gardner Fox and artist Harry Lampert, the original Flash first appeared in Flash Comics #1 (January 1940). (from Wikipedia)"
Article.create! name: "Splinter", author: "Hamato Yoshi", published_at: 3.weeks.ago, content: "Master Splinter, or simply Splinter, is a fictional character from the Teenage Mutant Ninja Turtles comics and all related media. (from Wikipedia)"

Populate the database.

rails db:seed

Create the articles controller with index, show and edit actions.

rails g controller articles index show edit

The implementation is straight forward.

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])
    if @article.update_attributes(allowed_params)
      redirect_to @article, notice: 'Article has been updated'
    else
      render :edit
    end
  end

  private

  def allowed_params
    params.require(:article).permit(:name, :author, :content, :published_at)    
  end
end

The article edit.html.erb looks like this:

<h1>Edit Article</h1>
<%= form_for @article do |f| %>
  <% if @article.errors.any? %>
    <div class="error_messages">
      <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
      <ul>
      <% @article.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :author %><br />
    <%= f.text_field :author %>
  </div>
  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_area :content, :rows => 12, :cols => 35 %>
  </div>
  <div class="actions"><%= f.submit %></div>
<% end %>

The articles index.html.erb looks like this:

<h1>Articles</h1>
<div id="articles">
<%= render @articles %>
</div>

The article partial _article.html.erb looks like this:

<h2>
  <%= link_to article.name, article %>
</h2>
<div class="info">
  by <%= article.author %>
  on <%= article.published_at.strftime('%b %d, %Y') %>
</div>
<div class="content"><%= article.content %></div>

The article show.html.erb looks like this:

<h1><%= @article.name %></h1>
<%= @article.content %>
<p>
  <%= link_to "Edit", edit_article_path(@article) %> |
  <%= link_to "Browse Articles", articles_path %>
</p>

Define the article resource in routes.rb.

Rails.application.routes.draw do
  resources :articles

  root to: 'articles#index'
end

The layout file application.html.erb looks like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Au5</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <div id="container">
      <% flash.each do |name, msg| %>
        <%= content_tag :div, msg, id: "flash_#{name}" %>
      <% end %>
      <%= yield %>
    </div>
  </body>
</html>

Authentication

Uncomment

gem 'bcrypt', '~> 3.1.7'

in Gemfile. Run:

bundle

Generate the user model:

rails g resource user email password_digest

The users controller looks like this:

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

  def create
    @user = User.new(allowed_params)
    if @user.save
      redirect_to root_url, notice: 'Thank you for signing up!'
    else
      render :new
    end
  end

  private

  def allowed_params
    params.require(:user).permit(:email, :password_digest)
  end
end

Run the migration.

rails db:migrate

The users/new.html.erb looks like this:

<h1>Sign Up</h1>
<%= form_for @user do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2>Form is invalid</h2>
      <ul>
        <% @user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation %>
  </div>
  <div class="actions"><%= f.submit "Sign Up" %></div>
<% end %>

Add signup link to the layout file.

<div id="user_header">
   <%= link_to "Sign Up", new_user_path %>
</div>

Signup as a new user.

rails g controller sessions new

The sessions/new.html.erb looks like this:

<h1>Log In</h1>
<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :email %><br />
    <%= text_field_tag :email, params[:email] %>
  </div>
  <div class="field">
    <%= label_tag :password %><br />
    <%= password_field_tag :password %>
  </div>
  <div class="actions"><%= submit_tag "Log In" %></div>
<% end %>

Add login link to the layout file:

<%= link_to "Log In", new_session_path %>

Define the sessions resource in routes.rb:

resources :sessions

Add create action to sessions controller:

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: 'Logged in!'
  else
    flash.now.alert = 'Email or password is invalid'
    render :new
  end
end

Login as the newly registered user. You will get the error:

undefined method `authenticate' for #<User:0x007f8>

Add:

has_secure_password

to user.rb. You will now get the error:

BCrypt::Errors::InvalidHash in SessionsController#create 

You can check the values of the user record in the rails console.

u = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<User id: 1, email: "bparanj@gmail.com", password_digest: nil, created_at: "2016-06-28 20:05:34", updated_at: "2016-06-28 20:05:34">

Since this record was created before we added the has_secure_password declaration, it fails. Create another user and login as that user. You will now get the error:

Error: Password can't be blank

In the log, error is:

Unpermitted parameters: password, password_confirmation

To fix this, in users_controller.rb add allowed_params method:

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

Use this method in the action. You can now register and login. We can automatically login the user after signup, in users_controller after saving the user:

session[:user_id] = @user.id

To logout a user, add destroy action to sessions_controller.rb:

def destroy
  session[:user_id] = nil
  redirect_to root_url, notice: 'Logged out!'
end

Change the navigation in the layout file:

<div id="user_header">
  <% if logged_in? %>
     <%= link_to 'Logout', session_path, method: :delete %>
  <% else %>
     <%= link_to "Sign Up", new_user_path %>
     <%= link_to "Log In", new_session_path %>       
  <% end %>
</div>

Add the logged_in? helper to application controller:

private

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

helper_method :logged_in?

Login and logout, you will see an error:

ActionView::Template::Error (No route matches {:action=>"show", :controller=>"sessions"} missing required keys: [:id]):
<%= link_to 'Logout', session_path('bogus-id'), method: :delete %>

You will now be able signin, signup, login and logout. Let's make URL's user friendly. Define the following routes in routes.rb:

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

Use the named routes in the layout file.

<% if logged_in? %>
  <%= link_to 'Logout', logout_path %>
<% else %>
  <%= link_to "Sign Up", signup_path %>
  <%= link_to "Log In", login_path %>        
<% end %>

You can download the source code for this article from au5.

Summary

In this article, you learned how to use ActiveModel has_secure_password to implement register, login, logout features in Rails 5.


Related Articles

Watch this Article as Screencast

You can watch this as a screencast Authentication from Scratch in Rails 5


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.