Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dhairyagabha/86f2ea1bcae40b034a9850ad2a2e695d to your computer and use it in GitHub Desktop.
Save dhairyagabha/86f2ea1bcae40b034a9850ad2a2e695d to your computer and use it in GitHub Desktop.
Rails Application with Devise and Two-Factor Authentication

Rails Application with Devise and Two-Factor Authentication

Prerequisites: Ruby, Rails, Active Record CRUD and Javascript

When you start learning web-development with rails, you start by understanding the fundamentals of an MVC Framework. Once through the basics, blog is the first application for most of us. But we evolve quickly into more complex applications, thanks to DHH and the Rails Community. Any application you build would require authentication and perhaps the ability for two-factor authentication for your users.

In this guide, we will start with a brand new rails application, will add devise and add two factor authentication to the application.

What will you learn:

How to create a brand new rails application

I am pretty sure you are already familiar with the process of creating a new rails app but just in-case.

Step 1: Go to the directory where you would like to create this project in your terminal and type below:

$ rails new two-factor-autnetication

If you are like me, you might also like to go ahead and add options to set up postgresql database, skip keeps and test unit to be able to configure RSpec later.

$ rails new two-factor-authentication -d=postgresql --skip-keeps --skip-test-unit

Step 2: In most cases, I also like to start with a UI Framework such as Bootstrap4. To add Bootstrap4, navigate to your project directory in terminal and type below:

$ cd two-factor-authentication/
$ yarn add bootstrap jquery popper.js

Add below config to config/webpack/environment.js:

const { environment } = require('@rails/webpacker')

const webpack = require('webpack')
environment.plugins.append('Provide', new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
  Popper: ['popper.js', 'default']
}))

module.exports = environment

Step 3: Add below config in app/javascript/packs/application.js to add bootstrap JS:

require("bootstrap/dist/js/bootstrap")

Note: No need to import jQuery and Popper once initialized as Webpacker plugin.

Step 4: Add below config in app/assets/stylesheets/application.css to add bootstrap CSS:

*= require bootstrap/scss/bootstrap

or below to import bootstrap CSS app/assets/stylesheets/application.scss:

@import "bootstrap/scss/bootstrap";

How to add devise

Now add the Devise gem to the Gemfile:

gem 'devise'

Run below in the terminal:

$ bundle install // Install the new devise gem

$ rails g devise:install // This will add the initializer file with all devise options

$ rails g devise User // Create a devise user model

The Devise installer will provide a set of instructions in the terminal, most important configuration is below: Add the following line of code to config/environments/development.rb:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

This will allow for it to send emails from localhost.

How to add additional information to a user when signing up

In order to add more details to the User model, add more options similar to below:

$ rails g devise User first_name:string last_name:string timezone:string admin:boolean

This will add following columns:

  • First Name (String)
  • Last Name (String)
  • Timezone (String)
  • Admin (Boolean)

The resulting migration should look something like below:

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.inet     :current_sign_in_ip
      # t.inet     :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      # Custom Columns
      t.string :first_name
      t.string :last_name
      t.string :timezone
      t.boolean :admin

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

How to enable various add-ons provided by devise for further security

There are several plugins available on devise that help manage credentials beyond the basics.

When a devise model is generated, it includes several plugins configured by default - :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable

Database Authenticatable: This enables the ability to authenticate a user via the database with their email address and password.

Registerable: This enables the abilty to be able to register users. This can be removed in a case where the users in the database are synced via a different system such as an employee system.

Recoverable: This enables the ability reset the password.

Rememberable: This enables the ability to remember the credentials so the user can be logged in easily.

Validatable: This provides validation for email and password.

Above are enabled by default to provide a good user experience. We are going to enabled the following in-addition to the default:

Trackable: This enables the application to track the IP address, sign in count and the timestamp.

Confirmable: This enables the requirement for confirming the email address when creating an account.

Lockable: This enables the ability to lock accounts after a set number of failed attempts.

Trackable

Add :trackable to devise call in the model file. Uncomment the following lines from the migration file to add necessary columns.

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.inet     :current_sign_in_ip
      t.inet     :last_sign_in_ip

Confirmable

Add :confirmable to devise call in the model file. Uncomment the following lines from the migration file to add necessary columns.

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

    # Uncomment the line below for the index on the confirmation token column
    add_index :users, :confirmation_token,   unique: true

Confirmable has some additional configurations that can be set in the devise initialization file. config.allow_unconfirmed_access_for = 2.days will allow the user to access the system without confirming their account.

config.confirm_within = 3.days will require the user to confirm their email within 3 days.

Lockable

Add :lockable to devise call in the model file. Uncomment the following lines from the migration file to add necessary columns.

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at
    
    # Uncomment the line below for the index on the unlock token column
    add_index :users, :unlock_token,         unique: true

Lockable has some additional configurations that can be set in the devise initilization file. config.lock_strategy = :failed_attempts will configure a lock strategy, it is default to failed attempts. config.maximum_attempts = 20 configured the maximum attempts, default to 20.

config.unlock_keys = [:email] will configure an what information will be used to unlock the account.

config.unlock_in = 1.hour configures how much time you have to unlock an account.


Add below config to the application controller to enable extra fields for user registration.

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller? # Enables additional parameters
  before_action :authenticate_user! # Requires authentication before any request.

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :timezone])
  end
end

Once all configuration is set and extra variables have been added, migrate the database:

$ rails db:migrate

How to add two factor authentication

Finally to the best part, lets look at how to get two factor authentication set up. Add below gems to the Gemfile

gem 'devise-two-factor'
gem 'rqrcode'

devise-two-factor is a barebones two factor authentication gem for devise. It will add all the necessary functionality. rqrcode is for generating the QR code that will allow you to set up Two-Factor Authentication for your application.

You can then follow the instructions on the devise-two-factor homepage here or follow below:

Step 1: Run below to create migration, add two factor configuration to the devise initializer and set plugin for the devise call in the User model.

rails generate devise_two_factor User TWO_FACTOR_ECRYPTION_KEY

Should generate below migration:

class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :encrypted_otp_secret, :string
    add_column :users, :encrypted_otp_secret_iv, :string
    add_column :users, :encrypted_otp_secret_salt, :string
    add_column :users, :consumed_timestep, :integer
    add_column :users, :otp_required_for_login, :boolean
  end
end

Should add below to app/models/user.rb:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :registerable,
         :recoverable, :rememberable, :validatable, :confirmable, :lockable, :trackable, 
         :two_factor_authenticatable, :otp_secret_encryption_key => ENV['TWO_FACTOR_ECRYPTION_KEY']
end

Add the otp_attempt to sign-in params in the application controller:

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?
  before_action :authenticate_user!

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :timezone, :user_type])
    devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
  end
end

Add the :otp_attempt to the filtered parameters list along with password.

Rails.application.config.filter_parameters += [:password, :otp_attempt]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment