Two Factor Authentication

Episode #46 by Teacher's Avatar David Kimura

Summary

Add multi factor authentication to your existing authentication solution. This gives your users the option to increase the level of security to their account and help prevent unauthorized access.
rails security authentication 9:13

Resources

Summary

# Gemfile
gem 'devise'
gem 'active_model_otp'
gem 'rqrcode'

# config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { sessions: 'users/sessions' }

  resources :users do
    member do
      post :enable_multi_factor_authentication, to: 'users/multi_factor_authentication#verify_enable'
      post :disable_multi_factor_authentication, to: 'users/multi_factor_authentication#verify_disabled'
    end
  end

  get :protected, to: 'visitors#protected'  
  root 'visitors#index'
end

Not mentioned in the episode, but within the documentation of RQRCode, you should add some styling for your QR code.

# application.css
.qr {
  border-width: 0;
  border-style: none;
  border-color: #0000ff;
  border-collapse: collapse;
}
.qr td {
  border-width: 0;
  border-style: none;
  border-color: #0000ff;
  border-collapse: collapse;
  padding: 0;
  margin: 0;
  width: 10px;
  height: 10px;
}
.qr td.black { background-color: #000; }
.qr td.white { background-color: #fff; }

# controllers/users/multi_factor_authentication_controller.rb
class Users::MultiFactorAuthenticationController < ApplicationController
  before_action :authenticate_user!
  before_action :set_user

  def verify_enable
    if current_user == @user && 
       current_user.authenticate_otp(params[:multi_factor_authentication][:otp_code_token], drift: 60)
      current_user.otp_module_enabled!
      redirect_to edit_user_registration_path, notice: 'Two Factor Authentication Enabled'
    else
      redirect_to edit_user_registration_path, alert: 'Two Factor Authentication could not be enabled'
    end
  end

  def verify_disabled
    if current_user == @user && 
       current_user.authenticate_otp(params[:multi_factor_authentication][:otp_code_token], drift: 60)
      current_user.otp_module_disabled!
      redirect_to edit_user_registration_path, notice: 'Two Factor Authentication Disabled'
    else
      redirect_to edit_user_registration_path, alert: 'Two Factor Authentication could not be disabled'
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end
end

# controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def create
    self.resource = warden.authenticate!(auth_options)

    if resource && resource.otp_module_disabled?
      continue_sign_in(resource, resource_name)

    elsif resource && resource.otp_module_enabled?

      if params[:user][:otp_code_token].size > 0
        if resource.authenticate_otp(params[:user][:otp_code_token], drift: 60)
          continue_sign_in(resource, resource_name)
        else
          sign_out resource
          redirect_to root_url, alert: 'Bad Credentials Supplied.'
        end
      else
        sign_out resource
        redirect_to root_url, alert: 'Your account needs to supply a token.'
      end

    end
  end

  private

  def continue_sign_in(resource, resource_name)
    set_flash_message!(:notice, :signed_in)
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end
end

# models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_one_time_password
  enum otp_module: { disabled: 0, enabled: 1 }, _prefix: true
  attr_accessor :otp_code_token
end

# migration file
# rails g migration add_otp_secret_key_to_users otp_secret_key:string otp_module:integer

class AddOtpSecretKeyToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :otp_secret_key, :string
    add_column :users, :otp_module, :integer, default: 0
  end
end

# devise/registrations/edit.html.erb
<div class="row">
  <div class="col-sm-4 col-sm-offset-4">
    <h1>Edit <%= resource_name.to_s.humanize %></h1>
    <hr>
    <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
      <%= devise_error_messages! %>

      <div class="form-group">
        <%= f.label :email %><br />
        <%= f.email_field :email, class: 'form-control'  %>
      </div>

      <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
        <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
      <% end %>

      <div class="form-group">
        <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
        <%= f.password_field :password, autocomplete: "off", class: 'form-control'  %>
      </div>

      <div class="form-group">
        <%= f.label :password_confirmation %><br />
        <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control'  %>
      </div>

      <div class="form-group">
        <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
        <%= f.password_field :current_password, autocomplete: "off", class: 'form-control'  %>
      </div>

      <div class="form-group">
        <%= f.submit "Update", class: 'btn btn-lg btn-block btn-primary' %>
        <%= link_to "#{@user.otp_module_enabled? ? 'Disable' : 'Enable'} Two Factor", 
                    '#two_factor', 
                    data: { toggle: :modal }, 
                    class: 'btn btn-lg btn-block btn-info' %>
      </div>
    <% end %>
  </div>
</div>

<div class="modal fade" id="two_factor">
  <% url = @user.otp_module_enabled? ? disable_multi_factor_authentication_user_path(@user) : enable_multi_factor_authentication_user_path(@user) %>
  <%= simple_form_for :multi_factor_authentication, url: url, html: { class: 'form-inline' }  do |f| %>
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
          <h4 class="modal-title"><%= @user.otp_module_enabled? ? 'Disable' : 'Enable' %> Two Factor Authentication</h4>
        </div>
        <div class="modal-body">
          <% unless @user.otp_module_enabled? %>
            <% qr = RQRCode::QRCode.new(resource.provisioning_uri, size: 10, level: :h ) %>
            <table class="qr" align="center">
              <% qr.modules.each_index do |x| %>
                  <tr>
                    <% qr.modules.each_index do |y| %>
                        <% if qr.dark?(x,y) %>
                            <td class="black"/>
                        <% else %>
                            <td class="white"/>
                        <% end %>
                    <% end %>
                  </tr>
              <% end %>
            </table>
            <hr>
          <% end %>
          <div class='form-group'>
            <div class='text-center'>
              <%= f.input_field :otp_code_token, placeholder: 'Verify Token', class: 'form-control input-lg' %>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <%= f.submit "Update", class: 'btn btn-lg btn-block btn-primary' %>
        </div>
      </div>
    </div>
  <% end %>
</div>

# devise/sessions/new.html.erb
<%= f.text_field :otp_code_token, placeholder: 'Token', class: 'form-control' %>