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.
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.
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.
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 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.
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.
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.
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 +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.
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!