Blog

Implementing 2FA with Authenticator Apps (TOTP) in Rails

Charles Martinez
April 4, 2023

Two Factor Authentication has been popular on applications to improve security. 2FA is implemented to better protect both a user’s credentials and the resources the user can access. And one popular method is using SMS OTP where you sign in to an application and you then receive an SMS with an OTP that you need to then enter into the application. But the thing is, SMS OTP is not cheap and thus would require subscription from a Third Party App like Twilio.

A free alternative method would then be TOTP (Time-Based One Time Password) which is mostly used by Authenticator Apps like Google Authenticator etc. TOTP is a type of two-factor authentication that uses a six-digit code (or one-time passcode, OTP) or one-time tokens generated by an authenticator app. The code changes every 30 seconds, so it’s unique for each login attempt.

Flow Chart of 2-Factor Authentication

To integrate TOTP 2FA in your Rails App, We would need the following dependencies

devise_two_factor

rotp

rqrcode

Adding Devise Two Factor

Assuming you are already using Devise for User Authentication, devise-two-factor works on top of devise to add 2FA functionality. Follow the gem installation to setup devise 2FA but to summarize, This gem would add this fields to your User table. And the most important part is generating the otp_secret for each users because the creation of the TOTP would be based from that secret


add_column :users, :otp_secret, :string
add_column :users, :consumed_timestep, :integer
add_column :users, :otp_required_for_login, :boolean

# Setting up 2FA for a user

current_user.otp_required_for_login = true
current_user.otp_secret = User.generate_otp_secret
current_user.save!

Adding ROTP and RqrCode Gem

ROTP is a gem used to generate and validate TOTP and is compatible with popular authenticator apps like Google Authenticator. And we will use the rqrcode gem to generate a QR code SVG based from the TOTP generated.


totp = ROTP::TOTP.new(current_user.otp_secret)
qr_link = totp.provisioning_uri(current_user.email)

qrcode = RQRCode::QRCode.new(qr_link)
@svg = qrcode.as_svg(
  color: "000",
  shape_rendering: "crispEdges",
  module_size: 3,
  use_path: true,
)

The QR Code then would be render in your application in order for the User to scan it using their Authenticator App

Given that you would just need to add a new page where the user can scan the code and set it up with their authenticator apps to require OTP for login.

Sample QR Code

ROTP and RQRCode Gems are compatible with popular authenticator apps like to mention a few:

  • Google Authenticator
  • Lastpass Authenticator
  • 2FAS Auth
  • FreeOTP
  • Aegis Authenticator
  • StepTwo

Applying 2FA with TOTP

Now were all set with generating the TOTP using the said libraries, We then need to have a custom devise session controller to implement our 2FA Process


class Users::SessionsController < Devise::SessionsController
...

  def create
    ...
  end
end

# routes.rb

devise_for :users,
           controllers: { sessions: 'users/sessions' }

Then we would need to customize the create action in the session controller to handle authentication whenever the user is required for otp


def create
  if otp_two_factor_enabled?
    authenticate_with_otp_two_factor
  else
    super
  end
end

private

def find_user
  if session[:otp_user_id]
    User.find(session[:otp_user_id])
  elsif user_params[:email]
    User.find_by(email: user_params[:email])
  end
end

def otp_two_factor_enabled?
  return false if find_user.blank?

  find_user.otp_required_for_login
end

Now whenever a user is required for 2FA, There would be two steps - Render the Page for the User to enter the code from the authenticator app - Validate the TOTP entered by the user


def authenticate_with_otp_two_factor
  user = self.resource = find_user

  if user_params[:otp_attempt].present? && session[:otp_user_id]
    authenticate_user_with_otp_two_factor(user)
  elsif user&.valid_password?(user_params[:password])
    prompt_for_otp_two_factor(user)
  else
    flash[:error] = "Invalid Password or Email"
    redirect_to new_user_session_path
  end
end

def prompt_for_otp_two_factor(user)
  @user = user

  session[:otp_user_id] = user.id
  render 'view_file_to_render_qr_code'
end

def authenticate_user_with_otp_two_factor(user)
  if valid_otp_attempt?(user)
    # Remove any lingering user data from login
    session.delete(:otp_user_id)

    sign_in(user, event: :authentication)
    flash[:success] = "Signed in successfully"
    redirect_to after_sign_in_path_for(user)
  else
    flash.now[:alert] = 'Invalid code'

    prompt_for_otp_two_factor(user)
  end
end

def valid_otp_attempt?(user)
  totp = ROTP::TOTP.new(user.otp_secret)
  totp.verify(user_params[:otp_attempt]).present?
end

Sample Prompt HTML


Please enter the code from your Authenticator App

<%= form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false }, method: :post) do |f| %>
<%= f.text_field :otp_attempt, label: 'Code', autocomplete: 'one-time-code', autofocus: true, required: true, hide_label: true, placeholder: "6 Digit Code" %> <%= button_tag(type: 'submit') do %> Verify <% end %>
<% end %>

And that’s it! Users that enabled 2FA on their account will be required to enter the TOTP from their Authenticator App before they are able to login. And this implementation can still be expanded like applying 2FA only every N days (e.g every 15 days) which we can cover on the next part of this blog.