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.