How to Implement Devise with 2-factor Authentication: Tutorial

Photo of Jakub Niechciał

Jakub Niechciał

Updated Feb 21, 2023 • 11 min read

Recently I had to create a simple feature - two-factor authentication (2FA) in a Rails application.

I began with some quick research to see what is available online and recommended, as well as to think about how it might fit in with our projects at Netguru. See how I came up with a solution for 2-factor authentication in Devise.

I was not very happy with the results of my search - the topic of 2FA is definitely not as widely discussed as Devise itself and, while there are many possible options, only a few of them are well-described. So I thought it would be helpful to write a blog post with a quick summary of my research as well as the solution that I eventually decided was the best.

Devise with Two-factor Authentication Using Twilio and Authy

Assumptions: you have Devise set up and working. All you want is to extend it by adding 2FA based on SMS.

Authy is a paid product, so it’s natural to expect very good documentation and tutorials from them. Here you will find an excellent blog post about integrating Authy with your app.

  • Pros: Integrating takes very little time - just a few lines of code to get the basic setup.
  • Cons: it only handles Authy and is quite expensive - it costs $0.09 for each authentication that requires SMS (if the user saves the device, this is no longer required) and $0.05 for each SMS sent. However, if all you need is to verify your MVP, it’s great to move as fast as possible and pay later.

Devise with Two-factor Authentication Using Google Authenticator - No SMS Support

Assumptions: you have Devise set up and working. All you want is to extend it by adding 2FA based on Google Authenticator.

Google Authenticator is one of many possible options for integrating Two Factor Authentication. It’s free, so it’s frequently used. As you can imagine, there’s a gem. Here you can read how to integrate the devise_google_authenticator gem.

  • Pros: Very easy to integrate.
  • Cons: Doesn’t support SMS and can be a little bit quirky to customise as you get everything by default.

Devise with 2-Factor-Authentication Using Twilio - SMS (Optional) and Google Auth (Customised) Support

Assumptions: you have Devise set up and working. You want to extend it by adding 2FA based on SMS and Google Authenticator, and you don't want to use Authy (due to its price). The following approach is working, customisable and quick but may demand more tweaking for production, mostly in terms of user experience.

This tutorial is made by me and aims to provide you with a well-documented approach to customising 2FA. Implementing the full feature should not take you more than 2-3 hours - if it takes longer, or you don’t understand something, please let me know on Twitter.

What we will use:

You will also need a Twilio account. You can register and simply use a trial account (but only for development!).

Install Two_factor_authentication

First, you will need to install the two_factor_authentication gem by following its README. Installation runs migrations that add some necessary columns to your model and the strategy to the Devise User class.

Additionally, you should add the has_one_time_password(encrypted: true) method to the User class that is responsible for complying with Devise.

Take a look at the commit with the code necessary for two_factor_authentication installation.

Set up Your Models

Assuming that you have Devise properly configured, you must add a couple of necessary columns to your User model:

  • two_factor_enabled as a simple flag to handle 2FA for a particular user;
  • unconfirmed_two_factor that will point out that the user has enabled 2FA, but has not confirmed by providing the proper code (from SMS or Google Authorizer);
  • phone_number to handle the user's phone number.

Take a look at this migration.

You need to define a method that will tell Devise when to use two factor - it's called need_two_factor_authentication. This method will return true if the account has enabled two factor authentication and has already confirmed the usage (by installing the Google Authorizer app or providing a valid and confirmed phone number). Want to see an example? There you go!

Next, we need to define some logic for handling the unconfirmed_two_factor column. Confirmation of two-factor authentication should be set on - either when the enabled state changes from false to true, or when the phone number with 2FA auth enabled is changed.

Let's wrap it in the ActiveModel callback before_save:

Extend Your Devise Registration Edit View

In this example, we will base the user editing on the basic Devise form. You can extend this behavior to some other controller in a different view, but the idea will stay the same.

First, you must allow params for the Devise update account action. You also need to generate Devise views and extend them by adding the form inputs. Lastly, remember to define the controllers key in the devise_for declaration in the routes.rb file.

The code for these three steps is available in this repo.

Handle the Confirmation Process

You might not know it, but the whole idea of two-factor authentication is standardised (well, like OAuth). It's basically a protocol to generate N-length codes based on a particular key/secret/current time. Google Authenticator is a mobile app that allows the user to fetch the key via QR code (the secret is still hidden on your server) and, based on that key, generate a new code every 30 seconds. Basically, it does not have to connect to your app after the first initialisation and every code generated by Google Authenticator will be valid for exactly 30 seconds.

First, we will add a very basic way of rendering a Google-ready QR code via its API (no API key required). So, what will the flow of confirmation look like? Whenever the user changes something in their settings and saves it, a Devise action will execute the after_update_path_for method to redirect the user. We will override this method and, based on the current unconfirmed_two_factor state, redirect the user either to root or to our shiny new confirmation form. See it working and check out the code:

Then, we will create two actions in RegistrationsController - one for rendering the QR code and a simple input to enter the code from Google Authorizer; and the second to process that form, validate the code and update the current user object. These two actions, as well as their view and router, can be found here.

How can we validate that the code the user has provided is valid? There is a simple method provided by the two_factor_authentication gem, called authenticate_otp, that accepts the code in the argument and validates it. We will define and use the method on the User class that updates the model if the passed code is valid.

See this method here:

Last, but not least, we can make some tweaks to our app. First, let's render a flash message that informs the user that they must confirm the two-factor process as soon as possible. Let's also add a link in the header to the user edit page. Easy, isn't it?

Add Twilio to This Process

We still haven’t used Twilio or SMS. Currently, the only way for the user to authenticate in a two-factor way is to use Google Authenticator. Now, let's add the ability to send the code via SMS, as an alternative to Google Authenticator. We’ll install twilio-ruby - the Ruby wrapper for the Twilio API. Once that’s done, all we need to do is to override the send_two_factor_authentication_code method in the User class that is provided by the two_factor_authentication gem and is empty by default. We will check if the phone_number is present and, if so, send a message with the current two-factor code (generated in real time). See the full commit and the excerpt here:

Did you notice the rescue block? We don't have any phone validation, and I am not a big fan of it - it's rather hard, as different users enter phone numbers in many different ways, like +48 123-456-789, 123456789, 123 456 789 and so on. Therefore, we choose to ignore the validity of the phone number unless Twilio has problem sending the SMS. If this happens, we just render a flash message with an appropriate alert. The user can still use Google Authenticator or just go back to the Edit User page and change the phone number.

Summary

My quick research into two-factor authentication has shown me that there are still topics that are not described well enough. This tutorial is not a deep exploration of the 2FA strategies, but rather a quick intro aimed at getting the feature to work.

What about the tutorial? We've built a basic implementation of two possible ways to handle 2FA - by SMS and by Google Authenticator. It doesn't take a lot of effort to get them working. If you didn’t understand something or the tutorial took you more than 2-3 hours - let me know!

Photo of Jakub Niechciał

More posts by this author

Jakub Niechciał

Jakub has obtained a Master’s degree at Poznań University of Technology in Control Engineering and...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business