
How To Build Your Own Authentication in Rails 5
Building your own authentication in a Ruby on Rails application is easy using existing components in Rails. You could use existing gems like Devise, which are awesome, but in some cases they do too much, and are hard to work with if you want to do any modifications to conventional login behaviors. Rails already has built in authentication called has_secure_password
, which is a module for Active Model classes that will take care of most of the hard work our authentication will require.
Setup
To demonstrate this, we'll create a new Ruby on Rails application:
$ rails new sample_app
$ cd sample_app
In the the Gemfile
, uncomment or add the following gem which handles encryption:
gem 'bcrypt', '~> 3.1.7'
Then run bundle:
$ bundle
We will then create a controller to represent our application to a logged-in user. We'll generate these base files and edit them later:
$ rails generate controller Home index
Users
Users are who we are authenticating. To login to our application, they will need an email and password, but for has_secure_password
, the only requirement on the User model is to have a password_digest
field. Additional validations will be added to the class:
- Password must be present on creation
- Password length should be less than or equal to 72 bytes
- Confirmation of password (using a password_confirmation attribute) (OPTIONAL)
Let's create the model by running the Rails scaffold generator, which will additionally add the forms, and views we'll use.
$ rails generate scaffold User email:uniq password:digest
$ rake db:migrate
The password:digest
will add has_secure_password
to our model, as well as add the extra attributes for password_digest
and password_confirmation
where needed in our forms and views.
In user.rb
we'll add validations so that emails are unique (although we did add unique to our database, so an error will be raised regardless), but this will raise an error for our form and inform the user that their email is not unique:
class User < ApplicationRecord
has_secure_password
validates :email, uniqueness: true
end
We can also see the automatically generated views/users/new.html.erb
(from our scaffold command above) our new User form:
<%= form_with(model: user, local: true) do |form| %>
<% if user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :email %>
<%= form.text_field :email %>
</div>
<div class="field">
<%= form.label :password %>
<%= form.password_field :password %>
</div>
<div class="field">
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Password confirmation is optional. We have included password_confirmation
in our form, which will be used and validated for, but if we were to exclude it, then no confirmation would be needed.
Sessions
We now create the Sessions controller and views to handle sessions.
$ rails generate controller sessions new create destroy
For logins, we need to the edit the new session login form app/views/sessions/new.html.erb
:
<p><%= 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 %>
</div>
<% end %>
<p>
Or,
<%= link_to "Sign Up", signup_path %>
</p>
We need to update our Sessions controller 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: "You are logged in."
else
flash.now[:alert] = "Your email or password is invalid"
render :new
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: "You are logged out!"
end
end
Routes
We need to set the routes for handling logins and logouts:
Rails.application.routes.draw do
root to: "home#index"
resources :sessions, only: [:new, :create, :destroy]
resources :users
get "signup", to: "users#new", as: "signup"
get "login", to: "sessions#new", as: "login"
get "logout", to: "sessions#destroy", as: "logout"
end
Application Controller
Add the following code to the application_controller.rb
to handle the current_user
as well as the protective call to trigger the check for a current_user
through the application. By using the before_action
, we can be assured that the entire application is now covered by authentication.
class ApplicationController < ActionController::Base
helper_method :current_user
before_action :authenticate_user!
def authenticate_user!
unless current_user
redirect_to login_path, notice: 'Please login'
end
end
def current_user
if session[:user_id]
@current_user ||= User.find(session[:user_id])
else
@current_user = nil
end
end
end
For some controllers and specific views, we need to exclude them from authenticate_user!
, which we can do in the individual controllers, which will be necessary in our Sessions Controller
:
class SessionsController < ApplicationController
skip_before_action :authenticate_user!
...
end
We can also exclude specific actions, which we need to do in our Users controller:
class UsersController < ApplicationController
skip_before_action :authenticate_user!, only: [:new, :create]
...
end
This way, no authentication is done for user registration, but you need to be a user to view the index
, show
, and destroy
actions.
Final Steps
The last thing we'll do is customize our Home index page to reflect that we've logged in and provide a link to logout:
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<% if current_user %>
Logged in as <%= current_user.email %>.
<%= link_to "Log Out", logout_path %>
<% else %>
<%= link_to "Log In", login_path %> or
<%= link_to "Sign Up", signup_path %>
<% end %>
<p><%= notice %></p>
To test it out, start the Rails server:
$ rails server
Then, in the browser, go to the root path localhost:3000
and it should redirect to localhost:3000/login
. You will need to register, so click sign up
. Once registered, you should be able login.
Carson R Cole