A common feature of most web applications is authentication, and the associated requirement for passwords to be stored securely. There are a number of gems available that can add this functionality to your app (the most well-known of these is probably Devise) however if your needs are relatively simple it’s quite easy to build this from scratch.
I needed to add simple authentication to the app I built as part of Tealeaf Academy’s Rails course and below is a broad overview of how I went about it (if you’re looking for a step-by-step guide, this Gist is a good place to start).
First up I added all the routes I would need for register, login and logout paths to the route file:
# config/routes.rb
get "/register", to: "users#new"
get "/login", to: "sessions#new"
post "/login", to: "sessions#create"
get "/logout", to: "sessions#destroy"
To secure users passwords I used the bcrypt-ruby gem. Bcrypt is a password-hashing algorithm used to generate a one-way hash of a user’s password, with this then stored in the database and used to authenticate the user. The alternative would be to store the password in plain-text however this is universally considered to be an extremely poor practice. Doing so would mean that if your database was compromised, the hacker would have access to unencrypted passwords and email addresses that users may use on other sites as well as yours.
The bcrypt-ruby gem is included in the Gemfile when you create new Rails project, however this line is commented out by default so I simply needed to uncomment it:
# Gemfile
gem 'bcrypt-ruby', '~> 3.0.0'
Next up, I created a users table with a :password_digest
column as this is where Bcrypt stores the one-way hash of a user’s password:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :username
t.string :password_digest
t.timestamps
end
end
end
I then of course needed a user model, in which I included the has_secure_password
method to enable authentication using Bcrypt:
# models/user.rb
class User < ActiveRecord::Base
has_secure_password
end
The has_secure_password
method includes some simple validations by default (a password must be present, must be less than or equal to 72 characters in length, and must match the value for :password_confirmation
) but you can add additional validations if necessary. You can also disable the default validations entirely by adding the validations: false
option when including the method.
In the user model I set the minimum length for a password and also added validations to :username
:
# models/user.rb
class User < ActiveRecord::Base
has_secure_password
validates :password, length: { minimum: 6 }
validates :username, presence: true, length: { maximum: 20 }, uniqueness: true
end
The next step was adding a users controller. The new
action renders the sign up form and the create
action creates a user with the parameters submitted through the form. Note that strong parameters need to be specified here to allow these values to be mass-assigned.
From RailsGuides:
With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted. This means that you’ll have to make a conscious decision about which attributes to allow for mass update. This is a better security practice to help prevent accidentally allowing users to update sensitive model attributes.
# controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
session[:user_id] = @user.id
flash[:notice] = "You're now registered"
redirect_to root_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:username, :password, :password_confirmation)
end
end
To manage users logging in and out I created a sessions controller and added the relevant logic:
# controllers/sessions_controller.rb
def new
end
def create
user = User.where(username: params[:username]).first
if user && user.authenticate(params[:password])
login_user!(user)
else
flash.now[:error] = "Invalid username or password"
render :new
end
end
def destroy
session[:user_id] = nil
flash[:notice] = "Logged out successfully"
redirect_to root_path
end
private
def login_user!(user)
session[:user_id] = user.id
flash[:notice] = "Welcome, you're now logged in"
redirect_to root_path
end
This is where has_secure_password
works its magic.
Lastly, I defined the current_user
method in the application controller to make the @current_user
instance variable available in the views whenever there is a user logged in:
# controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
In a later assignment I added two-factor authentication to the app using Twilio which was really cool, and I’ll detail how I did this in an upcoming post.