Ruby on Rails has_secure_password Tutorial

The has_secure_password method adds methods to encrypt a password using the bcrypt algorithm. Also, it adds methods to authenticate against such a bcrypt password. For this method to work, the user model must have a password_digest attribute. In this tutorial, we'll build a simple application with a user model, and we will discuss in detail how we can use the has_secure_password method in our application. We will also use sessions to allow users to log in and log out. We will use Rails 7.0.2.2 and Ruby 3.1.1.

Set Up the Application

Let's start by creating a new application. In the terminal, we type:

rails new my_application

Then we change the directory to its folder:

cd my_application

The bcrypt gem is not installed by default, so we need to uncomment this line in the Gemfile:

gem "bcrypt", "~> 3.1.7"

Then we run:

bin/bundle install

We need a home page for the application. Therefore we'll generate a controller with an index action:

bin/rails g controller Pages index

In the config/routes.rb file, we delete the auto-generated route for the index action and instead set the root route to it:

root 'pages#index'

Generate a User Scaffold

From the terminal, we type:

bin/rails g scaffold User

The user model has two attributes: email and password_digest. The password_digest field in the database is required by the has_secure_password method. The encrypted form of the password is saved in this field. The password that the user sets is never saved to the database. Let's generate a migration to add these fields:

bin/rails g migration AddColumnsToUsers email:string password_digest:string

Then we run the migration:

bin/rails db:migrate

Now we add the has_secure_password method in the app/models/user.rb file:

class User < ApplicationRecord
  has_secure_password
end

Now, we must edit the new user form to add three input fields corresponding to the user model: email, password, and password_confirmation:

<%= form_with(model: user) do |form| %>
  <% if user.errors.any? %>
    <div style="color: red">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
      <ul>
        <% user.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div>
  <p>
    <%= form.label :email %>
    <%= form.email_field :email %>
  </p>
  <p>
    <%= form.label :password %>
    <%= form.password_field :password %>
  </p>
    <p>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
  </p>
  <p>
    <%= form.submit %>
   </p>
  </div>
<% end %>

The password and the password_confirmation parameters are not in the strong parameters list in the users controller. So let's edit the user_params method to add them:

def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

Now, we can go to http://localhost:3000/users/new and add users.

FIND THE current user

Next, we'll talk about user authentication. First, we'll define a @_current_user method to find the user with the id stored in the session. In the application_controller.rb file in the app/controllers/ directory, we add the following private method:

private 
def current_user
  @_current_user ||= session[:current_user_id] &&
  User.find_by(id: session[:current_user_id])
end

And we set it as a before filter in the before_action method of the same controller. At the top, we put this line:

before_action :current_user

Create a Session Controller

Next, we want to add a mechanism to add and remove the user from the session. Let's create the sessions_controller.rb file in the app/controllers/ directory. In the terminal type:

bin/rails g controller Sessions

Then we add a new and a create method to it:

def new
end
def create
  user = User.find_by(email: params[:email])
  if user != nil && user.authenticate(params[:password])
    session[:current_user_id] = user.id
    redirect_to root_path
    flash[:notice] = 'Welcome back.'
  else
    redirect_to log_in_path
    flash[:notice] = 'E-mail and/or password is incorrect.' 
  end
end

The authenticate method returns the user if the password is correct and false otherwise. 

Let's define the routes to these actions in the config/routes.rb file:

get 'log-in', to: 'sessions#new'
post 'log-in', to: 'sessions#create'

In the app/views/sessions/ folder we create a new view where we put the login form:

<p style="color: green"><%= notice %></p>
<h1> Log In </h1>
<%= form_with url: log_in_path  do |form| %>
  
  <p><%= form.label :email %></p>
  <p><%= form.email_field :email, required: true %></p>
  <p><%= form.label :password %></p>
  <p><%= form.password_field :password, required: true %></p>
    
  <p><%= form.submit 'Log In' %></p>
    
 <% end %>

Finally, we want the user to be able to log out. We create a destroy action in the sessions controller:

def destroy
  session.delete(:current_user_id)
  @_current_user = nil
  redirect_to root_path
  flash[:notice] = 'Goodbye.'
end

Then we define a route to it:

delete 'log-out', to: 'sessions#destroy'

Add LogIn, LogOut, and SignUp buttons to the Home Page

On our home page, we put a login button and a signup button if there is no user in the session, and a logout button if the user is logged in.

<p style="color: green"><%= notice %></p>
<% if @_current_user %>
  <%= button_to 'Log Out', log_out_path(@_current_user), method: :delete %>
<%  else %>
  <%= link_to 'Log In', log_in_path  %>
  <%= link_to 'Sign Up', new_user_path %>
<% end %>

Conclusion

We set up relatively easy a simple user authentication using the Rails has_secure_password method and sessions.

Post last updated on May 13, 2023