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




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!

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 =
qr_link = totp.provisioning_uri(

qrcode =
@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

# 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?


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

def otp_two_factor_enabled?
  return false if find_user.blank?


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]
  elsif user&.valid_password?(user_params[:password])
    flash[:error] = "Invalid Password or Email"
    redirect_to new_user_session_path

def prompt_for_otp_two_factor(user)
  @user = user

  session[:otp_user_id] =
  render 'view_file_to_render_qr_code'

def authenticate_user_with_otp_two_factor(user)
  if valid_otp_attempt?(user)
    # Remove any lingering user data from login

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


def valid_otp_attempt?(user)
  totp =

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.