DEV Community

Cover image for Authentication with Bcrypt
Ana Nunes da Silva
Ana Nunes da Silva

Posted on • Originally published at ananunesdasilva.com

Authentication with Bcrypt

Authentication

Authentication is a process that confirms a user's identity - making sure they are who they say they are. We usually do this before granting access or allowing privileged actions.

If we take this concept from the online to the real world, a key (🔑) is one of the simplest forms of authentication there is. Anyone with a key should be granted access to a building/house/room/cabinet. If you have the key, you are an authorized user.

Going back to the web world, a key usually takes the form of credentials. The most common credentials are a combination of a username and a password.

How can we make sure that we are storing these credentials safely?

Storing passwords

It is a very bad practice to store passwords in plaintext. If the data gets compromised, the intruder will have access to all the users' passwords. And since people tend to use the same password in different places, it will compromise the users not only on your application but also on other applications as well. This poses a great security threat.

Instead, you should store user passwords as secure undeciphered strings. This way, if a hacker manages to get access to your database, only hashed passwords will be leaked and, in theory, the hacker will not be able to decode them (or at least it will have a really hard time).

Ok, but how can we create and store these secured passwords?

Hashing algorithms

A hashing algorithm is a complex mathematical function that transforms a string of data into a seemingly random output string of fixed length.

Here's the trick of hashing algorithms:

Usually, encryption means transforming the string temporarily until a key is used to transform it back to the original string. But a hashing algorithm is a one-way encryption. One way means that it is non-reversible. Even if you have access to the database, you cannot get the original password back. But if we cannot decrypt the password, how can we match it?

A hashing algorithm is deterministic, meaning that the same input will always return the same output. When a user writes their credentials on the login form and submits it, the password is hashed and compared to the stored hashed password. If they are the same, then the login is successful.

All the above works great in theory, though in practice things are a bit more complex. With time, hackers have built tools to decode passwords. I will not go into much detail here but some of the common strategies are:

  • Rainbow tables: databases that have been precomputed with the most commonly used passwords and their hashed values (remember that a password will always have the same hash). Hackers can use a hashed password to get the original string. These databases have grown to store billions of records! Hackers

  • Dictionary attack: An attempt to guess passwords by using well-known words or phrases. These words/phrases will be hashed and compared with the hash they are trying to decode.

  • Brute force attack: a trial and error attack. It is the most expensive/time-consuming strategy. It implies trying out all the possible variations of characters up to a certain maximum length until you eventually get one right.

But as these hackings develop, so do the hashing algorithms. There are different hashing algorithms out there, some not recommended anymore for having become vulnerable like MD5, SHA-1, or SHA-256. Which one should we use then?

Bcrypt

Bcrypt is a cryptographic hashing algorithm created in 1999, and designed with passwords in mind. There are two main characteristics that make Bcrypt safer than other algorithms:

  • Bcrypt is a slow algorithm - this is good, as it reduces the number of passwords by second that can be hashed by a hacker. Prevents dictionary attacks.
  • Bcrypt uses salt - a string that is appended to the hash and stored together. So decoding the password requires knowing the salt string. Prevents rainbow attacks.

Rails comes with Bcrypt support. The ActiveModel has a has_secure_password method that we can use to set and authenticate against a Bcrypt password. It will provide a set of methods that will help set up authentication.

Without further ado, let's see how it works:

Install bcrypt

Look into your Gemfile and you will see a commented line with the bcrypt gem. It is included by default when a new rails app is created.

Uncomment the line and install it.

#Gemfile

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem 'bcrypt', '~> 3.1.7'
Enter fullscreen mode Exit fullscreen mode

Generate a user model and corresponding db table

The password_digest attribute is what will store the hashed password.

rails g model User username:string password_digest:string
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

This is what the schema should look like:

# db/schema.rb
create_table "users", force: :cascade do |t|
    t.string "username"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
end
Enter fullscreen mode Exit fullscreen mode

Add the has_secure_password method to the User model

# models/user.rb
class User < ApplicationRecord
  has_secure_password
end
Enter fullscreen mode Exit fullscreen mode

The following validations are added automatically:

  • Password must be present on creation
  • Password length should be less than or equal to 72 bytes
  • Confirmation of password (using a XXX_confirmation attribute)

Create user/session routes

The following routes will allow us to sign up, log in, and log out:

# config/routest.rb
  resources :users, only: [:create]

  get '/signup', to: 'users#new'
  delete '/logout', to: 'sessions#destroy'
  get '/login', to: 'sessions#new'
  post '/sessions', to: 'sessions#create'
Enter fullscreen mode Exit fullscreen mode

Build the signup feature

# controllers/users.rb
class UsersController < ApplicationController
  def new; end

  def create
    @user = User.new(user_params)

    if @user.valid?
      @user.save
      redirect_to login_path
    else
      redirect_to signup_path
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password, :password_confirmation)
  end
end
Enter fullscreen mode Exit fullscreen mode
<%# views/users/new.html.erb %>
<%= form_for @user do |f| %>
  <%= f.label :username %>
  <%= f.text_field :username, placeholder: "Username" %>
  <%= f.label :company %>
  <%= f.text_field :company, placeholder: "Company Name" %>
  <%= f.label :password %>
  <%= f.password_field :password, placeholder: "Password" %>
  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation, placeholder: "Confirm Password" %>
  <%= f.submit "Create Account" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Note that when we create a new user, we do not ask for a password_digest but rather a password (and password_confirmation) - two attributes that were made available by the has_secure_password included before. Bcrypt will then convert the password string into a hash and store it in the password_digest.

Build the login/logout features

This is where another new method will be handy - the authenticate method. It will take the password string provided by the user, hash it and compare it with the stored hash.

Let's say I have a user with username 'ana' and password '1234' (not a safe one, I know, just to simplify the example):

When providing the wrong password:

User.find_by(username: 'ana').authenticate('dasdas')
# => false
Enter fullscreen mode Exit fullscreen mode

When providing the right password:

User.find_by(username: 'ana').authenticate('1234')
# => 
# #<User:0x00007fd0f635df8
#id: 3,
#username: "ana",
#password_digest: "[FILTERED]", 
#created_at: Thu, 07 Jul 2022 14:20:56.012150000 UTC +00:00,
#updated_at: Thu, 07 Jul 2022 14:20:56.012150000 UTC +00:00>
Enter fullscreen mode Exit fullscreen mode

If both are the same, then a user instance is returned and we can proceed with setting the session's user with the authenticated user session[:user_id] = @user.id). When logging out, we will just need to clear this user from the session (session[:user_id] = nil)

# controllers/sessions.rb
class SessionsController < ApplicationController
  def new; end

  def create
    @user = User.find_by(username: params[:username])
    if @user && @user.authenticate(params[:password])
      session[:user_id] = @user.id
      redirect_to schedule_path(@user.company.name)
    else
      redirect_to login_path
    end
  end

  def destroy
    session[:user_id] = nil

    redirect_to login_path
  end
end
Enter fullscreen mode Exit fullscreen mode
<%# views/sessions.new.html.erb %>
<%= form_tag sessions_path, remote: true do %>
  <%= label_tag "Username" %>
  <%= text_field_tag :username, nil, placeholder: "Username" %>
  <%= label_tag "Password" %>
  <%= password_field_tag :password, nil, placeholder: "Password" %>
  <%= submit_tag "Log In"%>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Require authentication

To be able to protect pages from unwanted visits, we can create an authenticate_user! method that will check if the user is logged in. If not, it will require the user to log in to be able to access that page.

For example:

class PostsController < ApplicationController
  before_action :authenticate_user!, only: [:show]

  def show
    @posts = Post.all
  end
end
Enter fullscreen mode Exit fullscreen mode
# controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :current_user

  def authenticate_user!
    redirect_to login_path unless current_user
  end

  def current_user
    @current_user ||=
      begin
        return nil unless session[:user_id]

        User.find(session[:user_id])
      end
  end
end
Enter fullscreen mode Exit fullscreen mode

Add login/logout buttons

Finally, we can add the login/logout buttons to our app:

<% if current_user %>
  <%= button_to "Logout", logout_path, method: :delete %>
<% else %>
  <%= button_to "Login Page", login_path, method: :get %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Happy authentication!

Top comments (0)