Authentication with Bcrypt
Ana Nunes da Silva
Posted on July 8, 2022
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'
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
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
Add the has_secure_password
method to the User model
# models/user.rb
class User < ApplicationRecord
has_secure_password
end
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'
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
<%# 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 %>
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
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>
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
<%# 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 %>
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
# 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
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 %>
Happy authentication!
Posted on July 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024