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>
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
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
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!"
endend
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:
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