OOZOU
Get in Touch
Back to Blog

OTP 2FA in Ruby on Rails with ROTP

Implementing two factor auth with one time passwords is a great way to add additional security for your users.

June 8, 2020
5 min read
OTP 2FA in Ruby on Rails with ROTP

What is OTP 2FA?

2FA or two factor authentication is when a system requires two steps to log a user in e.g. username and password, then a code sent via email or SMS. OTP 2FA or one time password 2FA is a more user friendly solution as the code is generated on the users device by sharing a secret between the application and the user. To configure OTPs the user will first scan a QR code that contains the secret, with an application like Google Authenticator or 1Password. Once configured the users device will be able to generate an OTP which will be valid for a specified window of time. Normally 15-30 seconds.

Getting set up

With the rtop gem OTP 2FA is really simple to setup. I’m going to assume you have a user setup with some pre-existing authentication system, most probably Devise. Let’s install rotp and rqrcode so we can generate qrcodes to scan from your favorite password manager.

# Gemfile
gem 'rotp'
gem 'rqrcode'

Configuring ROTP

To configure ROTP we need to add an otp_secret column and a last_otp_at column (integer, not date) to our User table which will be used to generate the next OTP for validation.

bin/rails generate migration add_otp_secret_to_users otp_secret:string last_otp_at:integer
bin/rails db:migrate

Generating the secret and verifying the user's first OTP

Now we’re going to need to allow users to configure OTPs. Where you do this in your app is up to you, from a UX point of view there are two options

  1. OTP protection is mandatory
    • After signing up to the app, redirect the user to an OTP configuration page where they can setup their OTP Secret
  2. OTP protection is optional
    • Allow the user to turn on or off OTP protection
      • After turning on, redirect to an OTP configuration page where they can setup their OTP Secret
      • After turning off, delete the OTP Secret

To create an otp_secret we need to generate a base32 string using the ROTP lib and then verify the user has setup their authenticator correctly before saving the secret to ensure we don’t lock the user out of their account. I will do this all in the controller so it’s easier to demo but I recommend abstracting somewhere else, for example form objects, service objects, or interactors.

# app/controllers/otp_secrets_controller.rb
class OtpSecretsController < ApplicationController
  def new
    @otp_secret = ROTP::Base32.random
    totp = ROTP::TOTP.new(
      @otp_secret, issuer: 'YourAppName'
    )
    @qr_code = RQRCode::QRCode
      .new(totp.provisioning_uri(current_user.email))
      .as_png(resize_exactly_to: 200)
      .to_data_url
  end

  def create
    @otp_secret = params[:otp_secret]
    totp = ROTP::TOTP.new(
      @otp_secret, issuer: 'YourAppName'
    )

    last_otp_at = totp.verify(
      params[:otp_attempt], drift_behind: 15
    )

    if last_otp_at
      current_user.update(
        otp_secret: @otp_secret, last_otp_at: last_otp_at
      )
      redirect_to(
        after_otp_configuration_path,
        notice: 'Successfully configured OTP protection for your account'
      )
    else
      flash.now[:alert] = 'The code you provided was invalid!'
      @qr_code = RQRCode::QRCode
        .new(totp.provisioning_uri(current_user.email))
        .as_png(resize_exactly_to: 200)
        .to_data_url
      render :new
    end
  end
end

The view

Now we need to create a form to display the new action above

# app/views/otp_secrets/new.html.erb
<%= form_tag otp_secrets_path, method: :post do %>
  <%= hidden_field_tag :otp_secret %>
  <%= image_tag @qr_code %>
  <%= text_field_tag :otp_attempt, label: "<strong>Verify (enter a one-time password)"</strong> %>
<% end %>

Now you should have something that looks like this That's it for setting up the user. Now we just need to take care of forcing the user to enter their OTP code when signing in

Verifying the OTPs on sign-in

If you are forcing all users to secure their accounts with OTPs it's as simple as adding a text input to the sign in form and validating it, however, if OTPs are not mandatory you will need to check if OTPs are enabled during sign-in and defer sign-in until the user successfully verifies an OTP. The logic to validate OTPs is pretty simple. We've already done it once, we need to modify it a little to include the last_otp_attempt

last_otp_at = totp.verify(
  otp_attempt, after: user.last_otp_at, drift_behind: 15
)
if last_otp_at
  user.update(last_otp_at: last_otp_at)
else
	# Return an error to the user
end

Additional thoughts

That's all there is to it. This article is enough to get you up and running however there are some other things to consider here:

  1. otp_secret should be encrypted to protect your users if your database is leaked. I use attr_encrypted.
  2. You should do some validation to ensure that the otp secret hasn't been tampered with, I like to user ActiveSupport::MessageEncryptor so I can be sure that what I've sent to the user is what is returning and then also decode and re-encode the secret to ensure it's really a base32 string.
  3. If you are using a two-step sign-in process, you should ensure that your users either:
    • are not actually signed in until you successfully validate an OTP.
    • or you can create a before_action to check that they have validated an OTP after their last sign in. The later being much more costly as it will be processed during each request.

Related Articles

How Can I Design a Software: A Beginners Guide for Business Owners

How Can I Design a Software: A Beginners Guide for Business Owners

Designing software is an exciting yet complex process that involves creativity, logical thinking, and problem-solving skills.

Read More →
The Future of Edge Computing: What Businesses Need to Know

The Future of Edge Computing: What Businesses Need to Know

The digital landscape is evolving rapidly, and businesses must adapt to keep up with rising demands for speed, efficiency, and real-time processing.

Read More →
The Future of Retail: AI, AR, and Digital Transformation Trends

The Future of Retail: AI, AR, and Digital Transformation Trends

Retail is evolving at an unprecedented pace, driven by AI (Artificial Intelligence), AR (Augmented Reality), and digital transformation technologies, revolutionizing the retail industry. Traditional brick-and-mortar stores are no longer just about selling products—they are becoming smart, interactiv

Read More →

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

Start a Conversationhello@oozou.com
OOZOU Logo

Bangkok · Singapore · Hong Kong

X
Facebook
Instagram
LinkedIn

Services

  • Web Development
  • AI Agents & Generative AI
  • Mobile App Development
  • Data Analytics & Engineering
  • UI/UX & Product Design
  • Digital Transformation

Company

  • About Us
  • Careers
  • Blog
  • Whitepapers
  • Case Studies
  • Today I Learned
  • Contact

More

  • Industries
  • Get an Estimate
  • Partner Network
© 2026 OOZOU. All rights reserved.
Privacy PolicyCode of ConductABAC Policy