paint-brush
Building a Simple Session Based Authentication using Ruby on Railsby@daniel-wesego
8,993 reads
8,993 reads

Building a Simple Session Based Authentication using Ruby on Rails

by Daniel WesegoApril 2nd, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Building a Simple Session Based Authentication using Ruby on Ruby on Rails is easy. We will use a simple session based authentication system to authenticate users and hold session data until they log out or close their browser. We have two models, User and Item. Users will have many items but they have to login to see their items. You should add tests, validation and other important standards when building your app. You can learn more about the REST approach from this Wikipedia article. Let's get started using rails 6.0.0.

Company Mentioned

Mention Thumbnail
featured image - Building a Simple Session Based Authentication using Ruby on Rails
Daniel Wesego HackerNoon profile picture

Building your first authentication system may look intimidating at first. But to be honest, it's really easy. After reading this article, you will know how easy it is to create a session based authentication in rails.

In this brief article, we will go over writing a session based authentication system to authenticate users and hold session data until they log out or close their browser. We will have two models, User and Item. Users will have many items but they have to login to see their items. We will build this step by step using rails 6. Let's get started!

OK, let's create a new rails app.

rails new simple_session
cd simple_session

After creating the rails app, let's continue by creating the models and controllers. We will just go over the basics in this article. You should add tests, validation and other important standards when building your app.

rails generate model User name:string email:string password_digest:string
rails generate model Item name:string user:references
rails db:migrate
rails generate controller Users
rails generate controller Items

We should add the 'bcrypt' gem to store password hashes in the database. So, let's do that.

#Gemfile
gem 'bcrypt'
bundle install

We will go with the easy way to use bootstrap. We will just paste the BootstrapCDN link in the head section of application.html.erb file. You can put your custom css in the custom.scss file created below.

#application.html.erb
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
touch app/assets/stylesheets/custom.scss

OK, let's now start working on the user and items controller. We will have 3 methods in the users controller - show, new , create. The show method will be used to show users, the new method will be used to present the form to create users, and the create method will be used to create users on the back-end. You can add other methods like index, edit, update, and destroy. But since this is a basic start up, we will skip those for the users controller. We will implement all those for the items controller. You can learn more about the REST approach from this Wikipedia article.

#users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the app!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

Let's add the views. We will have a header view that will be used by the application.html.erb file. The application.html.erb is the base view that will be used by all views.

#application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>RailsSession</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  </head>

  <body>
    <%= render 'layouts/header' %>
    <div class='container'>
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type %>"><%= message %></div>
      <% end %>
      <%= yield %>
    </div>
  </body>
</html>
#layouts/_header.html.erb
<nav class="navbar mb-2">
  <div class="container-fluid">
    <div class="navbar-header">
      <%= link_to "Items", '#' %>
    </div>
    <ul class="nav navbar-nav navbar-right">
      <li><%= link_to "Sign up", signup_path %></li>
    </ul>
  </div>
</nav>
#views/items/home.html.erb
<div class="text-center home">
  <h2>This is a simple guide on how to create session based authentication system</h2>
  <br/>
  <p>
    Please signup or login to test the app.
  <p>
  <%= link_to "Sign up", signup_path, class: "btn btn-primary" %>
</div>
#views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li class='text-danger'><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
#views/users/new.html.erb
<h1 class='text-center'>Sign up</h1>

<div class="row">
  <div class="col-md-4 offset-4">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'mb-1 form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'mb-1 form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'mb-1 form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'mb-1 form-control' %>

      <%= f.submit "Create my account", class: "mt-2 btn btn-primary" %>
    <% end %>
  </div>
</div>
#model/user.rb
class User < ApplicationRecord
  has_many :items
  has_secure_password
  before_save { self.email = email.downcase }
  validates :name, presence: true
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
  validates :password, presence: true, length: { minimum: 6 }
end
#config/routes.rb
Rails.application.routes.draw do
  root 'items#home'
  get '/signup', to: 'users#new'
  resources :users
end
#views/users/show.html.erb
<div class="row">
  <div class="offset-4 col-md-4">
      <h3 class='text-center'>
        <%= @user.name %>
      </h3>
      <h3 class='text-center'>
        <%= @user.email %>
      </h3>
  </div>
</div>

Ok, good work. We have finished user creation. Let's continue by creating the sessions controller.

rails generate controller Sessions

Let's add the following routes:

#config/routes.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'

Let's add the login form in the 'new.html.erb' file.

#views/sessions/new.html.erb
<h1 class='text-center'>Log in</h1>

<div class="row">
  <div class="col-md-6 offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: " mt-2 btn btn-primary" %>
    <% end %>

  </div>
</div>

Let's now implement the sessions controller. This part will be our main focus. We will first add the methods in the sessions controller. Not all the methods will be defined here. Some will be defined in the sessions helper. We will import the sessions helper in the applications controller which will be extended by all controllers automatically. Let's do this step by step.

Let's first add the new, create and destroy methods in the sessions controller. The new method will be used for displaying the form. The create method will create the session, and the destroy method will destroy or remove the session when the user logs out. Some of the methods are implemented in the sessions helper which we will create later.

#sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_back_or user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

Let's add the methods in the sessions helper.

#app/helpers/session_helper.rb
module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end

  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  def logged_in?
    !current_user.nil?
  end

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end

  def current_user?(user)
    user == current_user
  end

  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

The first method (log_in) accepts user and creates a session for the user. Rails makes this easy for us.

The rails way of creating a session is just using 'session[:user_id] = user.id'. This will create a session with the user_id. The current_user method will return the current user if there is one or if there is a session present. That means if a user is logged in, the current user will be the that user. The logged_in? method just return true or false based on whether there is a current user or not. The log_out method will log out the user by deleting the session and setting the current_user to nil. The current_user? method accepts user and returns true if it's the same with the current user or false otherwise. This can be useful to restrict a user from visiting other user's details. The next two methods are useful to redirect a non logged-in user to the first visited URL after logging in.

Then we will import the session helper in the application controller. That means methods like logged_in? or current_user will be available to all controllers. Let's do that.

#application_controller.rb
include SessionsHelper

  private
  def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
  end

The private method will be used to store location and redirect to the login form. After the user logs in, the user will be redirected back to the URL they tried to visit.

But now if we try it, a user can visit the anything without logging in. That's because we have to add this method to restrict users that are not logged in. Let's add the before_action filters in the users controller.

#users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:show]
...

The before_action method will be implemented before the user accesses the show method. It will go first to the logged_in_user method before going to the show method. The logged_in_user will redirect back to login form if the user hasn't logged in. We can also add another methods to be filtered in the before_action declaration, but we don't want that. The user signup should be accessed by anyone because it doesn't require login.

Now it asks you to login if you visit the user page (localhost:3000/users/:id). One bad thing is that if you signup, it will ask you to login. But it doesn't make sense to login again after signing up. Let's fix that by updating the create method in the users controller file and let's also add the login link at the top. I've added the updated _header.html.erb file.

#users_controller.rb
  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the app!"
      redirect_to @user
    else
      render 'new'
    end
  end
#views/layouts/_header.html.erb
<nav class="navbar mb-2">
  <div class="container-fluid">
    <div class="navbar-header">
      <%= link_to "Items", root_path, class: "mx-2" %>
    </div>
    <% if logged_in? %>
      <ul class="nav navbar-right ">
        <li><%= link_to "Log out", logout_path, class: "mx-2", method: :delete %></li> 
      </ul>
    <% else %>
      <ul class="nav navbar-right ">
        <li><%= link_to "Log in", login_path, class: "mx-2" %></li> 
        <li><%= link_to "Sign up", signup_path, class: "mx-2" %></li>
      </ul>
    <% end %>
  </div>
</nav>

Look at how we used the logged_in? method from sessions helper to identify if a user has logged in or not. So, we are done with our session. Let's finish this tutorial by adding the items controller.

Items belong to a user. A user will have many items. Rails makes it easy in creating associations.

#app/models/item.rb
class Item < ApplicationRecord
  belongs_to :user
  validates :name, presence: true
end
#items_controller.rb
class ItemsController < ApplicationController
  before_action :logged_in_user

  def home
  end

  def new
    @item  = current_user.items.new
  end

  def index
    @items = Item.all
  end

  def create
    @item = current_user.items.build(item_params)
    if @item.save
      flash[:success] = "Item has been created!"
      redirect_to @item
    else
      render 'new'
    end
  end

  def edit
    @item = current_user.items.find(params[:id])
  end

  def update
    @item = current_user.items.find(params[:id])
    if @item.update_attributes(item_params)
      flash[:success] = "Item updated"
      redirect_to @item
    else
      render 'edit'
    end
  end

  def destroy
    @item = current_user.items.find(params[:id])
    if @item 
      @item.destroy
      flash[:success] = "Item has been deleted"
    else
      flash[:alert] = "Error"
    end
    redirect_to root_path
  end

  def show
    @item = Item.find(params[:id])
  end

  private

  def item_params
    params.require(:item).permit(:name)
  end

end

Ok, we have finished the controller for items. The views are going to be similar to the user views and I will leave it as an exercise for the reader to practice it. You can also add and admin user because right now everyone can delete items. You can make it so that you a user can only delete his or her own items only. Thanks for reading the article. Contact me if you encounter any issue through email, github, linkedIn, or twitter. The repository for this article can be found here.