13

Adding Two-Factor Authentication(2FA) to ActiveAdmin auth in a Ruby on Rails we...

 3 years ago
source link: https://blog.kiprosh.com/adding-two-factor-authentication-2fa-for-activeadmin-auth-in-a-ruby-on-rails-web-application/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Published on 10 September 2020 in rails

Adding Two-Factor Authentication(2FA) to ActiveAdmin auth in a Ruby on Rails web application

To enhance the security of a web application having a user authentication workflow, we use a security method called 2FA. It is also known as Two Factor Authentication(type of Multi-Factor Authentication). In this blog post, we will see how to implement email-based 2FA in ActiveAdmin auth of a Ruby on Rails application.

In the email-based 2FA approach, when logging in with an email and password, an OTP will be sent on a registered email address. Upon entering the OTP, it will successfully authenticate and the session will be started.

Also, we will see the following additional functionality and customizations in this article:

  • OTP to auto-expire in 60 seconds.
  • OTP to be reset automatically after 60 seconds.
  • OTP authentication expires after 24 hours.

Steps to implement the 2FA

  1. Setup active_model_otp
   gem 'active_model_otp', '~> 2.0', '>= 2.0.1'

2. Create a migration

  • otp_secret_key
  • otp_auth_at
   $ rails g migration AddOtpSecretKeyAndOtpAuthAtToUser otp_secret_key:string otp_auth_at:timestamp

3. Add routes

   # config/routes.rb

   devise_scope :user do
     get '/admin/otp', to: 'users/sessions/otp_authentications#new', as: :admin_otp_page
     post '/admin/otp', to: 'users/sessions/otp_authentications#create', as: :admin_verify_otp
   end

4. Configure and add a "before" action

   # config/initializers/active_admin.rb

   config.before_action :authenticate_current_user_with_otp!
   # app/controllers/application_controller.rb

   def authenticate_current_user_with_otp!
     return if devise_controller? || current_user.otp_authenticated?

     redirect_to(admin_otp_page_path)
   end

5. Add Controller

   # app/controllers/users/sessions/otp_authentications_controller.rb

   # frozen_string_literal: true

   module Users
     module Sessions
       class OtpAuthenticationsController < ActiveAdmin::Devise::SessionsController
         prepend_before_action -> { authenticate_user!(force: true) }
         skip_before_action :require_no_authentication

         def new
           return unless otp_sent?

           current_user.send_otp_mail
           session[:otp_invalid_after] = Time.zone.now.advance(minutes: 1)
         end

         def create
           if current_user.authenticate_otp(params[:otp], drift: 1.minutes)
             current_user.touch(:otp_auth_at)
             redirect_to admin_dashboard_path
           else
             # set invalid OTP flash message
             render :new
           end
         end

         private

         def otp_sent?
           return true if session[:otp_invalid_after].nil?

           session[:otp_invalid_after] < Time.zone.now
         end
       end
     end
  end

6.  Add OTP page

   # app/views/users/sessions/otp_authentications/new.html.erb

   <%= form_tag admin_verify_otp_path do %>
     <h2>Enter OTP</h2>
     <h2>Please check your email for the OTP</h2>
     <%= text_field_tag 'otp', nil, options = { placeholder: 'One Time Password', size: '6', maxlength: '6' } %>
     <%= hidden_field_tag(:email, current_user.email) %>
     <%= submit_tag 'Submit OTP' %>
     <%= link_to 'Resend OTP', admin_otp_page_path %>
   <%= link_to 'Logout', destroy_user_session_path %>
   <% end %>

7. Add methods to verify auth and for the mailer

  # app/models/user.rb

  class User < ApplicationRecord
    OTP_AUTH_EXPIRES_IN = 24.hours

    has_one_time_password

    def send_otp_mail
      AdminMailer.user_otp(email, otp_code).deliver_now
    end

    def otp_authenticated?
      return unless otp_auth_at?

      otp_auth_at + OTP_AUTH_EXPIRES_IN > Time.zone.now
    end
  end

8.  Add AdminMailer

  # app/mailers/admin_mailer.rb

  class AdminMailer < ActionMailer::Base
    default from: DEFAULT_EMAIL_ADDRESS

    def user_otp(email, otp_code)
      @otp_code = otp_code

      mail(
        to: email,
        subject: 'Sign-in: Email verification',
        bcc: BCC_EMAIL_ADDRESS_FOR_OTP
      )
    end
  end
# app/views/admin_mailer/user_otp.html.erb

<h3>Verify your login</h3>
<h2><%= "Your OTP is #{@otp_code}" %></h2>

9.  If you're adding this to an existing User model you'll need to generate otp_secret_key with a migration like:

User.find_each { |user| user.update_attribute(:otp_secret_key, User.otp_random_secret) }

If you face any issue, drop a comment 📝 in the comments section below 👇 mentioning the issue and error-details if any.

We would ❤️ to hear from you. Thank you.

References


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK