Lesson Weekend

Authentication is the process of identifying users by matching sign-in credentials with user account data in the application. In this course section we’ll look at two ways to add authentication to our applications:

  1. The first is a "roll-your-own" approach using the bcrypt gem which allows us to build our authentication from scratch.

  2. The second approach will use the Devise gem, which adds authentication code to your app with minimal configuration.

Hashing and Salting Passwords


bcrypt provides a password-hashing algorithm that allows us to add secure authentication to our Rails sites.

A hash algorithm takes data (in this case, a password) and hashes it using an algorithm. A password hash combines a user’s password with a piece of random data known as salt. Once the salt is added, it can’t easily be removed. It’s similar to when you add salt to your food; once it’s been added and mixed in, it’s almost impossible to take out.

This way, even if hackers or other malicious users access the password table of your database, it would be extremely difficult to crack the passwords.

Let’s take a look at how salting is applied to the sign up and sign in process for a user.

  • When a user signs up for an account with a user name and password, the password is hashed using a salt and then stored in the database.

  • When a user signs in with a user name and password, the password is hashed using the same encryption algorithm. If the hashed password exactly matches the hashed password stored in the database, the user is authenticated and sign in is successful.

User Model


The bcrypt gem is automatically included in the Gemfile of a new Rails project. However, it’s commented out, so go ahead and uncomment bcrypt and bundle again.

We’ll need a User model where we can store the user’s email, password hash and password salt. (You may also want to create a username field as well.) Here’s our migration:

201704210326_add_users.rb
class AddUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.column :email, :string
      t.column :password_hash, :string
      t.column :password_salt, :string
    end
  end
end

Note that we’re adding a password_hash field and a password_salt field but we’re not adding a password field. We don’t want to store unencrypted passwords in our database!

Adding Methods to the User model


We need two methods in our User model: an encrypt_password() method for when a user signs up and an authenticate() method for when a user signs in. Here’s our user.rb model with our new methods:

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :password
  validates_confirmation_of :password
  validates :email, :presence => true, :uniqueness => true
  before_save :encrypt_password

  def encrypt_password
    self.password_salt = BCrypt::Engine.generate_salt
    self.password_hash = BCrypt::Engine.hash_secret(password,password_salt)
  end

  def self.authenticate(email, password)
    user = User.find_by "email = ?", email
    if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
      user
    else
      nil
    end
  end
end

Let’s go through this line by line.

  • First, we have attr_accessor :password. Even though we don’t want to save passwords to the database, we do need to temporarily store a user’s password when a user signs up or logs in.
    • A quick refresher: attr_accessor adds read and write methods for a password attribute. This is called a virtual attribute because the attribute won’t actually be persisted to the database.
  • We also have two validations:
    • The first validates both the presence and uniqueness of the email field. We don’t want multiple accounts with the same email so validating uniqueness is important.
    • The second validation checks that the user’s password has been confirmed. When we add a form later, we’ll use both a password field and a password_confirmation field. We don’t want users to accidentally add typos to their passwords!
  • Next, we have a before_save callback that calls the encrypt_password method before the user is saved to the database. This way a salt and hash are generated and then saved.
  • Our encrypt_password instance method uses two built-in bcrypt methods. First, we use bcrypt to generate a salt. Then we use the hash_secret method to mix password (our virtual attribute) with our generated password_salt. Once that’s complete, the user’s password_salt and password_hash will be saved to the database.
  • We also have an authenticate class method. We use a class method so we can do the following in our controllers: @user = User.authenticate(params[:email], params[:password]).
  • Our authenticate method makes a database query to find a user by the provided email address. If there’s a matching user in our database and if that user’s stored password_hash matches the entered password (which has been hashed and salted like the stored value in the database), then the method will return user. Otherwise, the method returns nil. This will become clearer when we add the code for sessions_controller.rb later in this lesson.

Experiment with other validations to check the user has entered a sufficiently complex password. Try adding validations that ensure a user has a password with special characters such as uppercase letters and numbers. Regular expressions are a great way to do this. (Note that just checking for mixed case and numbers in itself won't ensure a complex password; you'd also need to blacklist common passwords like "password", make sure the password is of sufficient length, and possibly require other special characters as well.)

Routes


Let’s add some routes:

config/routes.rb
Rails.application.routes.draw do
  get '/signup' => 'users#new'
  post '/users' => 'users#create'

  get '/signin' => 'sessions#new'
  post '/signin' => 'sessions#create'
  get '/signout' => 'sessions#destroy'
end

We could theoretically do resources :users and resources :sessions but we only need five routes for this lesson. Our first route is a GET 'signup' that will lead to a page with a form where users can sign up. Then, when the user submits the form, the submitted information will be posted to our /users route.

Now let’s take a look at the sessions routes. The first route leads to a page where users can sign in. The second posts the submitted information, creating a session. The final route will destroy the session.

So what exactly is a session and why do we need it?

Sessions

A session is simply a temporary connection where our application and a user can interact and communicate. Think of a secure web application as a building with many rooms. Some doors are locked while others aren’t. Anyone can go inside the unlocked rooms but only signed-in users are allowed into the locked rooms.

To get into the locked rooms, a person first needs to go to the front desk (our sign-in page) and get a temporary badge (the user session).

Now, each time the user tries to enter a locked room, security (our web application) can see the badge and let the user in. (This process is known as authorization. We’ll cover authorization more later.)

When the user leaves the building, the badge is left at the front desk and destroyed (the sign out process).

Without badges, security would need to call the front desk each time a user tried to open a locked door. This would be a terrible user interface for an application; imagine needing to enter your password each time you visit a new page!

Our user’s session needs to be semi-permanent like the badge in the analogy above. Once the session is created, it will store information about the signed-in user which the application can use to verify whether that user should be allowed to visit certain pages or perform certain actions. When the user leaves the application, the session is destroyed; this prevents other users from being able to reuse that “badge."

One other thing: sessions aren’t just for authentication. In fact, sessions are a very important concept in computer science and can refer to any situation where two communication endpoints open a connection in order to share information. We’ll also be using sessions in the next course section when we add a shopping cart to an e-commerce site.

Rails automatically provides a session hash for each user (regardless of whether that user is signed in). Information about this session is stored in a cookie by default. We can assign any value to the cookie we want. Here’s an example:

Rails Process Graphic

Here, we’ve used binding.pry to freeze the code so we can poke around. The binding.pry has been inserted into an otherwise empty index method. It doesn't matter where we put the binding.pry because the session hash is available everywhere in our application. You can test this yourself in any Rails application with pry.

When we type in session, we get an ActionDispatch::Request::Session object that we can manipulate. session[:school] doesn’t have a value yet, but we can add a value by doing the following: session[:school] = Epicodus. We’re now storing a value in this user’s session hash! we could do the same for session[:pet_name] or session[:favorite_color] or any other value we’d like to store.

To remove the value, we can do the following: session[:school] = nil. If we don’t change the value in the application, then the value will persist until the user deletes or alters the cookie (such as by clearing the cache).

Setting session[:school] isn’t all that useful. However, we can set a value for session[:user_id] when a user signs in and then set that value back to nil when the user signs out. In other words, we can use Rails’ built-in session functionality to create our virtual badge! Any values we attribute to the session will be made available to our application’s controllers and views. You’ll see just how useful this is in the next section of this lesson.

You can explore Rails sessions further in the Rails Documentation on Sessions.

Adding Controllers

We need two controllers: a users_controller.rb and a sessions_controller.rb. Let’s start with our user_controller.rb.

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      flash[:notice] = "You've successfully signed up!"
      session[:user_id] = @user.id
      redirect_to "/"
    else
      flash[:alert] = "There was a problem signing up."
      redirect_to '/signup'
    end
  end

  private

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

Let’s start with our create route since the new route won't have any new code. We’ll grab the user_params (email, password and password_confirmation) from our sign up form. Our before_save :encrypt_password callback will be triggered before the user is saved, ensuring that the user’s password is properly hashed.

Now it’s time to update our session: session[:user_id] = @user.id. This will allow us to check if a user is logged in, though we aren’t doing anything with this information just yet.

Let’s also build our sessions_controller.rb:

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    @user = User.authenticate(params[:email], params[:password])
    if @user
      flash[:notice] = "You've signed in."
      session[:user_id] = @user.id
      redirect_to "/"
    else
      flash[:alert] = "There was a problem signing in. Please try again."
      redirect_to signin_path
    end
  end

  def destroy
    session[:user_id] = nil
    flash[:notice] = "You've signed out."
    redirect_to "/"
  end
end

We don’t need a new route because we aren’t actually creating a new user here. (We’ll get to the forms in a moment.) The create route uses our authenticate class method. Now it should be clear why the method returns either user or nil. If there’s a user, then we’ll give that user a session[:user_id]. If not, the user will be redirected to the signin_path and prompted to try again.

When a user logs out, the destroy route is triggered and the session[:user-id] is changed to nil.

We now have everything we need for a user to sign up, sign in and sign out.

This is great, but how can we fully make use of session[:user_id]?

Adding Methods to the Application Controller

Let’s add some code to application_controller.rb and put all the pieces together. Any code that we add to application_controller.rb will be made available to all controllers. This should be clear from the first line of code in every controller. Here’s an example: class UsersController < ApplicationController. In other words, UsersController inherits from ApplicationController.

We’ll add a current_user method so our application can always check which user (if any) is signed in:

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  helper_method :current_user

  def current_user
    if session[:user_id]
      @current_user = User.find(session[:user_id])
    end
  end
end

There are a few new concepts here. The first is helper_method. By defining current_user as a helper method, we’re making it available to all controllers and views. For now, this is all you need to know about helper methods; there’s also a lesson later in this course section that explores helper methods in greater depth.

Our current_user method checks if the user has a session[:user_id]. We can then query the database to find the user and store it in the instance variable @current_user. Now we can do something like this in our controllers and views:

if current_user
  “You can do this!"
else
  “Go away!"
end

Let’s do one more thing with our current_user method before we move on. We have a great opportunity to refactor our code here. If the current_user method is called multiple times on a page, it will make a database query each time. That’s slow and unnecessary. If @current_user already has a value, we should just grab that value without querying the database again. We only need to make one small change to our code to fix this:

...
@current_user ||= User.find(session[:user_id])
...

Here we use the Ruby operator ||=, which combines or with equal. So what exactly does ||= do? Let’s simplify this a bit. a ||= b is shorthand for the following:

if a
  a = a
else
  a = b
end

Let’s apply this to our current_user() method. If @current_user has a value, great. There’s no need to query the database. However, if @current_user is nil, we’ll make a database query to set the value of @current_user. It’s a small change, but it’s a big improvement in our code.

Adding Forms

We’re almost done. Let’s add some views for our sign up and sign in forms:

app/views/users/new.html.erb
<h1> Sign up</h1>
<%= form_for @user do |f| %>
  <%= f.label "Email" %>
  <%= f.text_field :email %>
  <%= f.label "Password" %>
  <%= f.password_field :password %>
  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>
  <%= f.submit "Sign Up" %>
<% end %>
app/views/sessions/new.html.erb
<h1>Sign in</h1>
<%= form_tag signin_path do %>
  <%= label_tag :email %>
  <%= text_field_tag :email %>
  <%= label_tag :password %>
  <%= password_field_tag :password %>
  <%= submit_tag "Sign in" %>
<% end %>

A few things:

  • We use form_for @user in our sign up form because we need to create a new user. Generally, we'll use a form_for whenever we need to associate a model with a form. Since we are generally associating models with forms, we mostly use form_for. However, we don't need one for our sign-in form because we just need to POST some parameters so we can create a session. (You may want to review the Rails documentation on form helpers for more information.)
  • We use the password_field to hide field input.
  • We also have a passsword_confirmation field that will be used with validates_confirmation_of :password from our User model.

While we’re at it, let’s add a few lines of code to the <body> of application.html.erb. This code demonstrates just how helpful our new current_user method can be:

app/views/layouts/application.html.erb
  <body>
    <% if current_user %>
      <%= current_user.email %> | <%= link_to "Sign out", '/signout' %>
    <% else %>
      <%= link_to "Sign up", 'signup' %> | <%= link_to "Sign in", '/signin' %>
    <% end %>

    <%= flash[:alert] %>
    <%= flash[:notice] %>

    <%= yield %>
  </body>

If a user is signed in, the user’s email will show at the top of the page along with a sign out link. If the user isn’t signed in, there will be links to sign in and sign up at the top of the page. This is great functionality to add to a navbar.

Authorizing Routes

Let’s add one last thing to our application. Our users shouldn’t be able to visit certain pages if they aren’t signed in. How can we use our current_user helper method to make sure these users can’t visit secure pages? We need to authorize users to make sure they’re signed in first. If they aren’t, we’ll redirect them to another page.

Let’s add another method to application_controller.rb.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base

  …

  def authorize
    if !current_user
      flash[:alert] = "You aren't authorized to visit that page."
      redirect_to '/'
    end
  end

end

When the authorize method is called, the method will check if there’s a current_user. If there isn’t, the user will be redirected to the homepage.

Wait. Why isn’t authorize() a helper method like current_user? Well, we only need to use the authorize() method in our controllers, not our views, and application_controller.rb already provides all its methods to our other controllers via inheritance. Making current_user a helper method extends its functionality to our views, which is super useful for things like our rudimentary navbar. Our authorize() method doesn't need that additional functionality.

We can now implement the authorize() method in our controllers:

app/controllers/just_another_controller.rb
class JustAnotherController < ApplicationController
  before_action :authorize, only: [:secret]

  def secret
  end

end

before_action is a filter. This filter says: before performing a controller action, call the authorize() method, but only for the secret route. If a user isn’t signed in and tries to visit the secret route, the authorize() method will redirect the user to the home page.

Creating an Admin


At this point, we have basic authentication that determines whether a user can sign in or not. What if we also want to add an admin to the mix?

The easiest way is to add a boolean admin attribute to the User model.

Then, if we are checking whether someone should be allowed admin access (such as to an application dashboard), we can do the following:

if current_user && current_user.admin

If the admin attribute is true for the current_user, the condition will be true and you can give them access. Otherwise, the condition will be false and you can deny access.

For now, if you want to test this, it's fine if your sign up form has a dropdown or checkbox for admin privileges. However, as you might guess, that isn't very secure.

An alternative is to only allow admin privileges via the Rails console. Just update the user in the console so that the admin attribute is set to true.

Alternatively, you can create an admin dashboard where an admin can grant admin access to other users. Give access to the first admin via the Rails console, then that admin can update other users as necessary.

You will be expected to include admin privileges on this course section's independent project, but it's fine if the admin privileges can only be set through the Rails console. If so, make sure that you include instructions for setting up an admin in your README!

And that’s it. We just added some basic authorization to our application. Don’t forget to add integration testing for the sign in and sign up process!

In the next class session, make sure to roll your authentication from scratch with bcrypt. This will help you better understand how authentication works. If you choose to use bcrypt in the future (instead of Devise, which we’ll start using soon), then check out has_secure_password. This method provides additional functionality like a built-in authenticate() method.

Additional Resources


Lesson 2 of 27
Last updated July 14, 2022