Last Updated: January 11, 2017
· robertomiranda

Effortless Two-Factor Authentication in Rails

Today's web applications are facing all kind of security intrusions commonly derived from Password cracking attacks. The user itself could even write the password somewhere accessible to untrusted parties making it easy for identity thieves access private information or worst, take over the user account.

One of the most effective ways to address this situation is requiring additional secrets that real account owners could obtain from other channels after they signed in with their regular email and password. These additional secrets are known as One-Time-Passwords and are the keystone of Two Factor Authentication.

Implementing One-Time-Password should not be a painful task, this is why we are introducing the ActiveModel::Otp gem.

ActiveModel::Otp works on any Rails application and can be configured in just a few steps. First we're going to add a field to our User Model, so each user can have an otp secret key.

rails g migration AddOtpSecretKeyToUsers otp_secret_key:string
      invoke  active_record
      create    db/migrate/20130707010931_add_otp_secret_key_to_users.rb

We’ll then need to run rake db:migrate to update the users table in the database. The next step is to update the model code. We need to use hasonetime_password to tell it will be use TFA.

class User < ActiveRecord::Base

The has_one_time_password sentence provides to the model some useful methods in order to implement our TFA system.
The otpsecretkey is saved automatically when a object is created, otpsecretkey is generated according to RFC 4226 and the HOTP RFC. This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.

user = User.create(email: "hello@heapsource.com")
 => "jt3gdd2qm6su5iqh
  • Getting the current code (also you can send it via SMS)
user.otp_code # => '186522'
sleep 30
user.otp_code # => '850738'
  • Authenticating using a code
user.authenticate_otp('186522') # => true
sleep 30 # let's wait 30 secs
user.authenticate_otp('186522') # => false
  • Authenticating using a slightly old code
user.authenticate_otp('186522') # => true
sleep 30 # lets wait again
user.authenticate_otp('186522', drift: 60) # => true

Google Authenticator Compatible

The library works with the Google Authenticator iPhone and Android app, and also includes the ability to generate provisioning URI's to use with the QR Code scanner built into the app.

# Use you user's emails for generate the provision_url
user.provision_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn'

# Use a custom fied for generate the provision_url
user.provision_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'

This can then be rendered as a QR Code which can then be scanned and added to the users list of OTP credentials.

Working example

Scan the following barcode with your phone, using Google Authenticator.


Now run the following and compare the output:

require "active_model_otp"
class User
  extend ActiveModel::Callbacks
  include ActiveModel::Validations
  include ActiveModel::OneTimePassword

  define_model_callbacks :create
  attr_accessor :otp_secret_key, :email

user = User.new
user.email = 'roberto@heapsource.com'
user.otp_secret_key = "2z6hxkdwi3uvrnpn"
puts "Current code #{user.otp_code}"

You can fork the Google Authentication application for iPhone & Android and customize it.

We'll probably enhance the gem with nice rails generators and client libraries for iOS and Android. Follow this blog and to get updates on this gem and other open-source initiatives of our company.

Say Thanks

3 Responses
Add your response

over 1 year ago ·

Beautiful! We've considered adding two-factor auth to our authentication solution Userbin (https://userbin.com) which is based on Rails. Will play around with this tomorrow. Great work!

over 1 year ago ·

Hi Robert, I am using ActiveModel_otp but now sure if it has support for 4 digit pin? As I see that ROTP supports 4 digit option.

Also I am not seeing your latest changes related to PADDING true by default in version 1.0.0


over 1 year ago ·