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

$ 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>

If you like this post I’d like to ask you for a favour:
Create an account at my open-source business network https://www.vutuv.de

Thank you and see you there!

Password Digest

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

And run bundle afterwords:

$ bundle

User Model

$ 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

$ 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

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

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

Screencast: Log in and Log out

Shameless Plug

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

Contact Information

Ruby on Rails, Phoenix Framework, WebPerf and Photography. Father of two. German and English.