Authentication from Scratch with Rails 5.2

A tutorial to create a simple authentication for your Rails 5.2 application when gems like Devise are too big or too complicated to customize.

Background: Often I use Devise as the one-stop-shop solution. But lately I found myself (again) in a Rails application trying to find out how to customize Devise for the specific needs of that application. Just to realize that I would have saved so much time by just implementing the authentication from scratch by myself.

The groundwork for tutorial was done by Ryan Bates many years ago.

Important: This tutorial works under the assumption that the session encryption of Rails is secure. If you disagree with that assumption you are bright enough to add an additional random token.

Green Field

We start with a fresh Rails application:

$ rails new shop
$ cd shop

Later we are going to redirect to root. So we start with creating an empty root page:

$ rails g controller home index

Please add the following code to config/routes.rb :

Rails.application.routes.draw do
root ‘home#index’
end

And some content for that page in the file app/views/home/index.html.erb :

<p id=”notice”><%= notice %></p>
<h1>Example</h1>
<p>Lorem ipsum …</p>

Password Digest

Obviously we do not store the clear text password in the database but a digest of it. For that we need to activate the bcrypt gem in the file Gemfile:

# Use ActiveModel has_secure_password
gem ‘bcrypt’, ‘~> 3.1.7’

And run bundle afterwords:

$ bundle

User Model

Now we create a User scaffold. Feel free to add any additional fields you might need (e.g. first_name, last_name). I just use email:uniq to store the email address (and create an unique database index) and password:digest to create a password_digest field in the new users table.

$ rails g scaffold User email:uniq password:digest
$ rails db:migrate

The digest part puts some Rails magic into action. The Rails generator creates a password_digest field in the table and asks for an additional password_confirmation in the form and the controllers user_params without you having to do anything extra. has_secure_password in the model takes care of encrypting the password and provides theauthenticate method to authenticate with that password.

Before a first test we need to add some validations in app/models/user.rb to make sure that we have an email address and that it is unique:

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

Now we can fire up Rails and create a new user in the browser:

$ rails s
Screencast: Create a new user at http://localhost:3000/users/new

Let’s just check how Rails stores the password digest in the table:

$ rails c
Running via Spring preloader in process 3204
Loading development environment (Rails 5.2.1)
>> User.first
User Load (0.2ms) SELECT “users”.* FROM “users” ORDER BY “users”.”id” ASC LIMIT ? [[“LIMIT”, 1]]
=> #<User id: 1, email: “sw@wintermeyer-consulting.de”, password_digest: “$2a$10$t6Q2R.N5fevFjhL/W1X.EulEJQ8TDWIzCvHpbDrAtQo…”, created_at: “2018–09–18 12:13:26”, updated_at: “2018–09–18 12:13:26”>

So only the digest is saved. Everything is secure. But we still need to create some sort of login to actually use it.

Sessions

When a user logs in he/she creates a new session. When the same user logs out that session gets destroyed. Therefor we create a sessions controller with three actions:

$ rails g controller sessions new create destroy

We put the following code into app/controllers/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: "Logged in!"
else
flash.now[:alert] = "Email or password is invalid"
render "new"
end
end
  def destroy
session[:user_id] = nil
redirect_to root_url, notice: "Logged out!"
end
end

As you can see we use session[:user_id] to store the logged in user id. In case you haven’t worked with sessions yet have a look at https://guides.rubyonrails.org/security.html#sessions

We need to put this code for the form in app/views/sessions/new.html.erb :

<p id=”alert”><%= 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 “Login” %>
</div>
<% end %>

Routes

The routes are a bit cumbersome. But we can fix this with this code in config/routes.rb :

Rails.application.routes.draw do
root ‘home#index’

resources :users
resources :sessions, only: [:new, :create, :destroy]
  get ‘signup’, to: ‘users#new’, as: ‘signup’
get ‘login’, to: ‘sessions#new’, as: ‘login’
get ‘logout’, to: ‘sessions#destroy’, as: ‘logout’
end

Now a user can use http://localhost:3000/login to login and http://localhost:3000/logout to logout. Much easier for everybody.

current_user

In most Rails applications the logged in user is available with a current_user helper. This come handy too if you want to use an authorization gem like cancancan. The most popular way to add this functionality is this code in app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
helper_method :current_user
  def current_user
if session[:user_id]
@current_user ||= User.find(session[:user_id])
else
@current_user = nil
end
end
end

To put it into use we change the content of app/views/home/index.html.erb:

<% if current_user %>
Logged in as <%= current_user.email %>.
<%= link_to “Log Out”, logout_path %>
<% else %>
<%= link_to “Sign Up”, signup_path %> or
<%= link_to “Log In”, login_path %>
<% end %>
<p id=”notice”><%= notice %></p>
<h1>Example</h1>
<p>Lorem ipsum …</p>

The End

And here is the screencast where I log in with my account and log out afterwards:

Screencast: Log in and Log out

Shameless Plug

Do you want to learn more about Rails 5.2? Buy my book at https://www.amazon.com/Learn-Rails-5-2-Accelerated-Development/dp/148423488X

In case you need consulting or training:
https://www.wintermeyer-consulting.de

Contact Information

Email: sw@wintermeyer-consulting.de
Twitter: https://twitter.com/wintermeyer