Rails Authentication (ft. bcrypt)
Turn on phone, enter passcode, open browser, go to email, login, check information, logout, go to facebook, login, check newsfeed, logout, and REPEAT. Our daily life consists of logging in/out to access our data. Data is sensitive and we don’t want anyone just browsing through our stuff. This is why authentication is so important.
In a Rails app, we can use bcrypt to protect our users from evil hackers. bcrypt is a hashing algorithm. A hashing algorithm takes in a string of any size and outputs a fixed-length string. Other hashing algorithms include md5 and sha1. The point of hashing algorithms is that you can convert a string to a hash, but it is nearly impossible to go from the hash back to the original string. This increases security so that passwords aren’t as easily deciphered.
Each time you enter a word such as “hello” into bcrypt you will get the same hash, no matter how many times you do it. This is a very useful property that hashing algorithms do. In practice, the hash of the password is called a password digest.
md5 and sha1 are pretty fast hashing algorithms, but bcrypt is designed to be slow and that’s a good thing because it protects our users from deadly hackers. The reason we want bcrypt to be slow is because sometimes our databases get compromised, and because it is designed to be slow, creating a hacking dictionary will take a very long time. bcrypt will slow down the process in which hackers can figure out a password. This is the whole reason to why bcrypt is designed to be slow.
However, hashing a password is not enough to protect against cyber attacks. You can never assume the database is impenetrable, no matter how many precautions you take. Cybercriminals have workarounds to just about anything. Thus, salting is also included when we use bcrypt.
What does salting a password mean? A salt is a random string that is attached to a hash. By hashing a plain text password with a salt, the hash algorithm’s output is no longer predictable. This salt is automatically included with the hash, so you do not need to store it in a database.
Here is an example of what a bcrypt “hash” might look like stored in a database:
Now that we know what bcrpt does, how do we go about implementing it into our Rails app? Let’s first visit the gem file. There, we should find a gem for bcrypt. bcrpt is usually commented out, so to activate it, you would just have to uncomment out bcrypt and run bundle update. After doing so, has_secure_password has to be added to the user model. has_secure_password is a macro that is meant to be implemented in the ActiveRecord model.
Now we should focus on the migrations. Create a migration for ‘createusers’ with t.string :password_digest included:
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest t.timestamps
end
end
end
Thus, on our table in the database, the name of one of the columns is password_digest. Password digest contains two attributes, which are password and password confirmation.
Now, run the migration with rails db:migrate and we can double check this is working in the console by running ‘rails c’.
Create a user with the following attributes:
User.create(name: 'Winston', email: 'winston@winston.com', password: 'winston')
After creating the user, this should come up:
Notice that password_digest is filtered. This is because we don’t want to save plain text password anymore, so we will be encrypting it with bcrypt.
Now, we can check that our models and migrations are working by entering the following commands in the console:
User.first.password => nil
User.first.password_digest => string (how is saved in database)
We can also note that the password_digest is not equal to the password that we created:
"winston" == User.first.password_digest =>false
Because we created a user model with has_secure_password, we have access through bcript to certain methods that we can use on our model. One of these methods is called authenticate:
User.first.authenticate('winston') => get back the winston object
After double checking that everything is working, we can create a new controller for our users with the following command:
rails g controller users
We want to create a page in which users can create an account. Here is what we need in the controller to do so:
class UsersController <ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params) if @user.valid?
@user.save
session[:id] = @user.id redirect_to user_path(@user)
else
flash[:errors] = @user.errors.full_messages
redirect_to new_user_path
end
end def show
@user = User.find(params[:id])
end def user_params
params.require(:user).permit(:name, :email, :password, :password confirmation)
end
Next, add a new.html.erb view file and include the following:
<h1> This is my new form </h1><%= form_for @user do | f | %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= 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 %>
<% end %>
And don’t forget to create the routes:
Rails.application.routes.draw do
resources :users, only: [:new, :create, :show]
end
We also want to include validations for the form:
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
validates :password, presence: true #validate :custom_method = custom method to check password_confirmation and password BEFORE creating has_secure_password
end
Also throw in some flash errors for a better user experience. Remember, flash errors can survive one extra http request. This is added into the new.html.erb file:
<h1> This is my new form </h1><% if flash[:errors] %>
<% flash[:errors].each do |error | %>
<li style="color:red"> <%= error %> </li>
<% end %>
<% end %><%= form_for @user do | f | %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= 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 %>
<% end %>
Lastly, we want to create a show form to show each user’s account with their own sensitive data:
<h1> User: <%= @user.name %> </h1><hr/><h3> Sensitive user data </h3><%= button_to "Log out", logout_path, method: :delete %>
WE ARE NOW DONE WITH CREATING ACCOUNTS! But.. how do we allow users to access their data by signing in and out? You guessed it, we have to create a session, so that when a user signs in, they create a new session and hold value in that session. We can know who is logged in at a given time by incorporating a sessions controller. Use the command ‘rails g controller sessions’ to create a sessions controller. We then have to add the following information into the controller:
class SessionsController < ApplicationController
def new
@user = User.new
end def create
@user = User.find_by(email: params[:user][:email])
if @user && @user.authenticate(params[:user][:password])
session[:id] = @user.id
redirect_to user_path(@user)
else
flash[:errors] = ['Email or password is incorrect']
redirect_to login_path
end
end def destroy
session.clear
redirect_to login_path
end
end
Again, we need to add routes for session:
Rails.application.routes.draw do
get '/login', to: 'sessions#new', as: 'login'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy', as: 'logout'
end
and create a log in page using a form:
<h1> Log in Page </h1><% if flash[:errors] %>
<li style = "color:red"> <%= flash[:errors] %> </li>
<% end %> <%= form_for @user, :url => '/login' do | f | %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit %>
<% end %>
For extra security (authenticate path) to prevent people from accessing info without being logged in, we need to implement a before action. To do this, we first have to add some methods into the ApplicationController:
class ApplicationController < ActionController::Base def current_user
User.find_by(id: session[:id])
end def logged_in?
!!current_user
end def not_logged_in
redirect_to login_path unless logged_in?
endend
Now add the before action in the sessions controller:
class SessionsController < ApplicationController before_action :not_logged_in, only: [:show] def new
@user = User.new
enddef create
@user = User.find_by(email: params[:user][:email])
if @user && @user.authenticate(params[:user][:password])
session[:id] = @user.id
redirect_to user_path(@user)
else
flash[:errors] = ['Email or password is incorrect']
redirect_to login_path
end
enddef destroy
session.clear
redirect_to login_path
end
end
This is the flow, or process, that one should follow when they want to create a secure user experience. A user can now create a password using bcrpt encription and securely log on to their account. Success!