Simple authentication in Rails

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 signup 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.