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
- 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
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!).
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
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
Set up Your Models
Assuming that you have Devise properly configured, you must add a couple of necessary columns to your
two_factor_enabledas a simple flag to handle 2FA for a particular user;
unconfirmed_two_factorthat will point out that the user has enabled 2FA, but has not confirmed by providing the proper code (from SMS or Google Authorizer);
phone_numberto 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
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
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.
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
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.
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!