Two Factor Authentication

#46 Two Factor Authentication
9/11/2016

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.
8
rails security authentication

Summary

Gemfilegem 'devise'
gem 'active_model_otp'
gem 'rqrcode'
config/routes.rbRails.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.rbclass 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.rbclass 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.rbclass 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 migraiton 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 "[email protected]_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' %>
Photo
Barry Allen said 3 months ago:

Thanks for sharing, any suggestions for rails 4? It seems that enum _prefix is not implemented yet in rails 4.

635114?v=3&s=64
kobaltz said 3 months ago:

You could add https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb to your lib folder and have it loaded in your path on the app boot. It should give you the functionality of enum prefix. I did something similar like this before I had fully upgraded a few Rails 4 apps to Rails 5.

Login to Comment