# 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">×</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' %>