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.
- How to create a brand new rails application
- How to add devise
- How to add additional information to a user when signing up
- How to enable various add-ons provided by devise for further security
- How to add two-factor authentication
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";
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.
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
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.
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
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.
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
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]