Here is how you can implement session-based authentication functionality in your rails application without using any gem.
Create author resources
Run the commands below.
$ rails generate model Author
$ rails generate controller Authors name:string email:string password_digest:string
$ rails generate migration add_index_to_authors_email // Add index
$ rake db:migrate
Set validations
Add validations for name
and email
.
# models/author.rb
class Author < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
end
Add secure password to Author
You'll have your users put the password and its confirmation in the form and send them as hashed values. (Hash values can not be decrypted even though they got intercepted by a third party during the transmission.)
You check if the sent hashed value matches the hashed password stored in the DB. And if it does, you allow your user to log in to the application.
Add has_secure_password
It's quite easy to set up in rails. Simply put has_secure_password
in the Author model. (Also add the minimum length of each password.)
class Author < ApplicationRecord
#
# other code
#
validates :password, length: { minimum: 6 }
has_secure_password
end
has_secure_password
- Enables you to store the hashed password in your DB as password_digest
- Lets you use password and password_confirmation params and validations for them.
- Lets you use the
authenticate
method.
Add bcrypt gem
Add gem 'bcrypt'
to your Gemfile and run bundle install
.
gem 'bcrypt'
Check if it's working correctly
Run the commands in the rails console to see if you can use the authenticate
method.
The authenticate
method returns false if the given password was wrong and returns the author object if the given password was correct.
$ Author.create(name:"test", email:"test@email.com", password:"000000")
$ Author.first.authenticate('test')
//=> false
$ Author.first.authenticate('000000')
//=>
#<Author:0x0000560ee2e0a1b8
id: 1,
name: "test",
email: "test@email.com",
password_digest: "$2a$12$bQQu49N3xNCKO8StooXLBOqwwCAv7NbPqt3aG35AFDHRUgh.C8BgO",
created_at: Mon, 30 Sep 2019 08:40:11 UTC +00:00,
updated_at: Mon, 30 Sep 2019 08:40:11 UTC +00:00>
Sign up functionality
Let's start by setting up the routes for users to sign up.
Rails.application.routes.draw do
resources :authors
get '/signup', to: 'authors#new'
post '/signup', to: 'authors#create'
Add the code below to the author controller.
class AuthorsController < ApplicationController
def show
@author = Author.find(params[:id])
end
def new
@author = Author.new
end
def create
@author = Author.new(author_params)
if @author.save
redirect_to @author
else
render 'new'
end
end
private
def author_params
params.require(:author).permit(:name, :email, :password, :password_confirmation)
end
end
Lastly, create a signup page and show page for each user under views/authors/
.
# views/authors/show.html.erb
<%= @author.name %>
<%= @author.email %>
# views/authors/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@author) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
Sign in/out
HTTP
is a stateless protocol. So we use sessions to maintain the user state.
The new
action is used to put information for a new session and create
action is used to actually create a new session. And the destroy
action is used to delete a session.
Set up routes
Set up routes for sessions
.
Rails.application.routes.draw do
resources :authors
# Create new users
get '/signup', to: 'authors#new'
post '/signup', to: 'authors#create'
# Sessions
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
end
Create a session controller
$ rails generate controller Sessions
Add the code to SessionsController
.
class SessionsController < ApplicationController
def new
end
def create
author = Author.find_by(email: params[:session][:email].downcase)
if author && author.authenticate(params[:session][:password])
log_in author
redirect_to author
else
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end
And add the code to SessionHelper
and include session helper in ApplicationController
.
The session
used in the code below is the built-in session
method in Rails.
module SessionsHelper
def log_in(author)
session[:author_id] = author.id
end
def current_author
@author ||= Author.find_by(id: session[:author_id]) if session[:author_id]
end
def logged_in?
!current_author.nil?
end
def log_out
session.delete(:author_id)
@current_author = nil
end
end
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
Remember me functionality
First of all, add a column called remember_digest
to Author
.
$ rails generate migration add_remember_digest_to_users remember_digest:string
Update code in the Author model. Each method has its description in the code.
class Author < ApplicationRecord
attr_accessor :remember_token
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
validates :password, length: { minimum: 6 }
has_secure_password
class << self
# Return the hash value of the given string
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Return a random token
def generate_token
SecureRandom.urlsafe_base64
end
end
# Create a new token -> encrypt it -> stores the hash value in remember_digest in DB.
def remember
self.remember_token = Author.generate_token
update_attribute(:remember_digest, Author.digest(remember_token))
end
# Check if the given value matches the one stored in DB
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
def forget
update_attribute(:remember_digest, nil)
end
end
Update the session helper.
module SessionsHelper
def log_in(author)
session[:author_id] = author.id
end
def current_author
if (author_id = session[:author_id])
@current_author ||= User.find_by(id: author_id)
elsif (author_id = cookies.signed[:author_id])
author = User.find_by(id: author_id)
if author && author.authenticated?(cookies[:remember_token])
log_in author
@current_author = author
end
end
end
def logged_in?
!current_author.nil?
end
# Make the author's session permanent
def remember(author)
author.remember
cookies.permanent.signed[:author_id] = author.id
cookies.permanent[:remember_token] = author.remember_token
end
# Delete the permanent session
def forget(author)
author.forget
cookies.delete(:author_id)
cookies.delete(:remember_token)
end
def log_out
forget(current_author)
session.delete(:author_id)
@current_author = nil
end
end
Update the session controller.
class SessionsController < ApplicationController
def new
end
def create
author = Author.find_by(email: params[:session][:email].downcase)
if author && author.authenticate(params[:session][:password])
log_in author
params[:session][:remember_me] == '1' ? remember(author) : forget(author)
redirect_to author
else
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end
Lastly, add remember_me
checkbox in the view.
<div class="login-form">
<h2>Log in</h2>
<%= form_for(:session, url: login_path) do |f| %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: 'Email', class: 'login-input'%><br/>
<%= f.password_field :password, autocomplete: "current-password", placeholder: 'Password', class: 'login-input' %>
<div class="check-field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<%= f.submit "Log in", class: 'btn btn-outline-primary login-btn' %>
<% end %>
</div>
Authorization
Add the following methods to the author controller.
class AuthorsController < ApplicationController
before_action :authenticate_author
##
Other code
##
private
def author_params
params.require(:author).permit(:name, :email, :password, :password_confirmation)
end
def authenticate_author
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
def correct_author
@author = Author.find(params[:id])
redirect_to(root_url) unless current_author?(@author)
end
end
Add the current_author?
method to the session helper.
module SessionsHelper
def current_author?(author)
author == current_author
end
end
That's it! Now you should have a simple authentication functionality on your rails app!
Top comments (3)
This is definitely an article with valuable information that I was looking for. So I'd love to thank you for the effort you've made in writing this information. smash karts
This tutorial provides a clear and detailed approach to implementing session-based authentication in a Rails application without Football Bros using any gems, giving users a deeper understanding of the security mechanism and how the system works.
Thanks for sharing. lovely informative codes.. It appears that you are asking about implementing session-based authentication for a website. Cookie Clicker Unblocked
Some comments may only be visible to logged-in visitors. Sign in to view all comments.