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
has_one_time_password
end
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")
user.otp_secret_key
=> "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
has_one_time_password
end
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.
Written by Roberto Miranda
Related protips
3 Responses
github repository https://github.com/heapsource/active_model_otp
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!
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
Thanks
Shivani