One of the very first articles I ever published was a simple little piece called Authentication in Rails 3.1+. That article is still surprisingly useful to this day, even though Rails has changed drastically. I figure 5 years later it's time for a follow up. In this article I will show you how to build an authentication system from scratch...well almost.
You see, Rails actually performs a lot of the heavy lifting for us with the help of a tiny library known as bcrypt. We only need to perform a few minor steps in order to enable this functionality. These steps are outlined below, but first: A few definitions are in order for the new folks.
Password Hash A password hash is a secure way to represent the password without needing to know what the password is. For example, using a simple hashing algorithm, the password 'foo' would be equal to
acbd18db4cc2f85cedef654fccc4a4d8. This random series of letters and numbers is not reversible by any standard means. This is different from encryption, where all data can be decrypted.
Password Salt - A password salt is a series of characters that is usually inserted at the beginning of the password in order to randomize the hash and make it it harder to guess. Password salts are used when storing passwords in order to make it harder to perform brute force attacks on our hashed passwords.
This article should work with minimal modifications on Rails 4 and up, but we use Rails 5 here. If you have problems with an earlier version of Rails, feel free to leave a comment below. Let's get started!
Rails Application Setup
First, we need to add the
bcrypt gem to our Gemfile. The bcrypt library is used to hash and salt passwords in a secure way. Why do we need to do this? Storing the passwords in plain text is considered to be a bad security practice. If your website gets compromised and the database gets copied, the attackers now have the credentials of all of your users. In addition, encryption is considered to be a poor security practice because anyone with the encryption key can easily decrypt the entire list of passwords. You also shouldn't try and reinvent the wheel with your own way of storing passwords. Doing so without understanding all the nuances of security can leave you open to an attack. Instead, let's use an existing, proven library to take care of the dirty work for us.
Let's start by addding the
bcrypt gem to our
bundle to install the gem.
There isn't anything else to do as far as setup goes at this point. Rails takes care of integrating
bcrypt. I will now show you how to build out the rest of your application.
Creating Our User Model
First, you will need to create a model to store user information. The User model will contain a couple of different attributes. The
password_digest attribute will store the hashed, salted password. Run the command below to create the User model.
rails g model User email password_digest
Great, now run
rails db:migrate to migrate the database.
Now it's time to add some code to our model to support the authentication system. Open up the User model at
app/models/user.rb and modify it so that it looks like the code listed below.
class User < ApplicationRecord has_secure_password validates :email, uniqueness: true, presence: true end
has_secure_password do? It adds two additional properties to your user:
password_confirmation. In addition, it adds a number of different validations (presence, length with a maximum of 72, and confirmation). Finally, it adds a method called
authenticate which allows you to authenticate the user against the database. Let's move on.
validations: falseto the
has_secure_passwordmethod. This will tell Rails not to add ANY validations to the password and password confirmation fields though, so use with caution.
In this example we will have a total of three different controllers. The first controller will be called
dashboard controller and associated
show view will display a simple welcome message and require a valid user. The second controller will be called
sessions controller will allow the user to log in and out of the site via the
destroy views. The third controller will be called
users controller will handle new user registration via the
create methods. Run the commands below to create these controllers.
rails g controller dashboard show rails g controller sessions new create destroy rails g controller users new create
Now let's update our routes file. Open the
config/routes.rb file and modify it so that it looks like the code below.
Rails.application.routes.draw do root 'dashboard#show' get 'users/new' get 'login', to: 'sessions#new' get 'logout', to: 'sessions#destroy' resources :sessions, only: [:create] resources :users, only: [:new, :create] end
Let's break this down. Line 2 sets the site root to the
show view of our Dashboard controller. Lines 5 and 6 define custom
/logout urls that map to our sessions controller's
destroy methods. Lines 7 and 8 set up a few other default routes that we will need. Let's move on.
Now I'll show you how to require a logged in user. First, open up
app/controllers/application_controller.rb and modify it so that it looks like the code below.
class ApplicationController < ActionController::Base before_action :require_valid_user! def current_user if !session[:user_id].blank? @user ||= User.find(session[:user_id]) end end def require_valid_user! if current_user.nil? flash[:error] = 'You must be logged in to access that page!' redirect_to login_path end end end
So what does the code above do? The
current_user function at lines 4-8 returns the currently logged in user or
nil if the user isn't logged in. The
require_valid_user! function at lines 10-15 redirects the user to the login page if he/she is not logged in. Line 3 calls this function by default for every page in the example.
Now let's work on the sessions controller. The sessions controller, as mentioned earlier, contains methods that are used to log in/out of the example. Open up
app/controllers/sessions_controller.rb and modify it so that it looks like the code listed below.
class SessionsController < ApplicationController skip_before_action :require_valid_user!, except: [:destroy] def new end def create reset_session @user = User.find_by(email: session_params[:email]) if @user && @user.authenticate(session_params[:password]) session[:user_id] = @user.id flash[:success] = 'Welcome back!' redirect_to root_path else flash[:error] = 'Invalid email/password combination' redirect_to login_path end end def destroy reset_session end def session_params params.require(:session).permit(:email, :password) end end
Line 2 ensures that we aren't requiring the user to be logged in for the
create method at Lines 7-19 handle the process of logging in:
- Line 8 resets the user's session. This helps prevent session fixation and other attacks.
- Line 9 finds the user by his/her email address.
- Lines 11-18 checks if the user exists and they are allowed to authenticate. If not, an error is displayed and the user is redirected to the login page. If the user was successfully authenticated they are redirected to the dashboard page.
destroy function at lines 21-23 resets the user's session, causing them to be logged out.
Now I'll show you how to handle new user registration. Open up
app/controllers/users_controller.rb and modify it so that it looks like the code listed below.
class UsersController < ApplicationController skip_before_action :require_valid_user! before_action :reset_session def new @user = User.new end def create @user = User.new(user_params) if @user.save flash[:success] = 'You have successfully created an account. Please sign in to continue.' redirect_to login_path else render :new end end def user_params params.require(:user).permit(:email, :password, :password_confirmation) end end
So what's going on here? Line 2 ensures that our session is clean before we register. Line 3 of the users controller skips the code that requires the user to be authenticated. The
create function at lines 10-19 handles new user registration. Once the user successfully creates an account, they are redirected to the login page. Pretty simple right? Let's move on.
Now that our models and controllers are out of the way, we can work on our views. First, let's work on the application layout. We only need to make a small change. We will include the CSS file for the popular Bootstrap library in order to improve the look/feel of our example. Open up
app/views/layouts/application.html.erb and modify it so that it looks like the code listed below.
The only thing we do here is link to the Bootstrap 4.1.2 file on the Bootstrap CDN and wrap the rendering of our views in a
div with a class of
container. Let's move on.
Now I'll show you how to create the dashboard page that we will use for this example. Open up
app/views/dashboard/show.html.erb and modify it so that it looks like the code listed below.
Welcome to your dashboard.<% if flash[:success] %><%= flash[:success] %><% end %> <%= link_to 'Click here to log out.', logout_path %>
The page is simple. It has a bit of text, renders the flash success message if it is populated, and has a link to log out.
Now I'll show you how to create the login/logout pages. First let's work on the login page. Open up
app/views/sessions/new.html.erb and modify it so that it looks like the code listed below.
Please log in to continue.<%- if flash[:error] %><%= flash[:error] %><% elsif flash[:success] %><%= flash[:success] %><% end %> <%= form_for :session, url: sessions_path do |f| %><%= f.label :email %> <%= f.email_field :email, class: 'form-control', placeholder: 'Email' %><%= f.label :password %> <%= f.password_field :password, class: 'form-control', placeholder: 'Password' %><%= f.submit 'Log In', class: 'btn btn-primary' %><% end %>
New User? <%= link_to 'Click here to create an account.', new_user_path %>
Let's break this down:
- Lines 4-8 display any flash messages that may be present.
- Lines 10-22 defines a form that we will use to login:
f.email_fieldfunction at line 13 creates a text box that has additional keyboard features on mobile devices. This includes an
@symbol on the main keyboard and some other features depending on your mobile device. Other than the custom keyboard, the email field behaves just like a regular text box.
f.password_fieldfunction at line 17 creates a text box that masks the password so that it cannot be viewed on screen.
f.submitfunction at line 20 submits the form.
- Line 25 contains a link to our new user registration page.
Next, I'll show you how to create the logout page. Open up
app/views/sessions/destroy.html.erb and modify it so that it looks like the code listed below.
<h1>Log Out</h1> <p>You have successfully logged out. <%= link_to 'Click here', login_path %> to log in again.</p>
This view is pretty simple. it contains a bit of text along with a link to log in again.
Now I'll show you how to create the new user registration page. Open up
app/views/users/new.html.erb and modify it so that it looks like the code listed below.
<h1>Sign Up</h1> <p>Create a new account using the form below. All fields are required.</p> <% if @user.errors.any? %> <div class='alert alert-danger'> <p>Please correct the following errors:</p> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <%= form_for @user do |f| %> <div class='form-group'> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> </div> <div class='form-group'> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> </div> <div class='form-group'> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> </div> <%= f.submit 'Register', class: 'btn btn-primary' %> <% end %> <p class='mt-2'> Already have an account? <%= link_to 'Click here to sign in.', login_path %> </p>
So what's going on here? Lines 4-13 display any validation errors that may be present. At line 15, we create a form for the user. You'll notice we have an email field along with two different password fields here, one for password and one for password confirmation. Finally we have a "Register" button that submits the form. At the very bottom, we have a link to login that we can click if we already have an account.
Building out our own authentication system is pretty simple. In future articles I will show you how to add more features. For now, start your Rails development server and try it out! Thanks for reading!