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?
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?
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 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:
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'
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
# 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)
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'
# 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_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
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 %>
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.
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
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 %>