Background
There are many gems for implementing two-factor authentication for Rails app out there. But, most of them don't work properly with Devise out from the box.
Some gems offering seamless integration with Devise gem never work smoothly with our existing Rails app. This is why we share our experience and strategy to implement two-factor authentication using Devise and ActiveModel::Otp gems.
This implementation, based on our experience, works very well with the latest Google Authenticator app of both iPhone and Android (version 5.10 as of 2020/06). The rqrcode gem does a nice job to provide us with QR code that's always scannable by the Google Authenticator.
Oh, we run this on Ruby 2.6.6 and Rails 6. So, we can guarantee it's working with the latest Rails version at the time we write this (2020/06).
1. Prepare Gemfile
You need to add several gems to achieve this. If you have an existing Rails app with Devise already, then feel free to skip the devise gem and add the rest of them.
In that example, we use the latest version of every gem. We recommend you do this on your app, as long as it is possible.
Once you added those required gems on Gemfile, you'll need to run bundle install as usual.
If you encounter a dependency issue on installing those gems, we also recommend updating all of your gems to its latest version first.
2. Update your users_controller.rb
We use users_controller.rb in this example - the controller and user.rb mode are generated by the devise gem. There, we need to add two methods to do these:
- Handle the page for a user to activate the two-factor-authentication. The page will include QR code (thanks to rqrcode gem) so users can scan this QR code using their Google Authenticator app. Below, we call it def activate_2fa
- Handle the activation process for two-factor-authentication once a user submits the response code from the Google Authenticator app. For this, we call it def activate_2fa_update
Feel free to adjust the example codes accordingly. You may extend it to have a user an option to de-activate the two-factor-authentication after they activated it.
We render QR code as svg image in this case. Based on our experience, this is the most stable way to make the Google Authenticator app recognize it on various devices and browsers.
3. Provide the HTML view for activation page
On the previous step, we have def activate_2fa in users_controller.rb. The method supposed to display the QR code and the form for a user to activate two-factor-authentication on our app.
So, we need to define the HTML view for this method like this:
We call html_safe on @svg variable so it will render the QR code in the browser, instead of rendering it as a text.
Below it, we render a simple form to receive the response code from the Google Authenticator app. In this form, a user will input the code they get from Google Authenticator and click the submit button.
Then, the form will send the data to def activate_2fa_update method in users_controller.rb (see step 2). That method will check if the response code (sometimes we call it otp or "one time password"). If the response code is correct, it will enable the two-factor-authentication.
4. Override devise gem sessions_controller.rb
Suppose a user has activated the two-factor-authentication in our app. Now, we want them to input the OTP after a user login successfully. This way, in case someone steals their passwords and tries to hijack their account, their account will keep secure. Because the hijackers still need another "key" to take over the account: the OTP generated by Google Authenticator app on the legitimate user's phone.
There are various strategies to handle this. Here's our example.
Here's what we do in that gist:
- First, we override the sessions controller of the devise gem. We only override def create, though.
- If a user successfully signs in and they have not activated two-factor-authentication on their account
- If a user successfully signs in and they have activated it, we'll render a form for them to ask them inputting the OTP code they see on their Google Authenticator app
- Once they input it, if the OTP is valid we let them in.
- If the OTP isn't valid, we let them try again for 5 times (in our example above)
- If they input a wrong password, we bring them back to the login page, to ask them to try again, as usual.
5. HTML view for the users to input their OTP code
Here's the HTML view for them to input their OTP code. This is an HTML view for that two_factors_authentication on step 4.
It's just simple form but there's an important thing to notice here. We append the current user's email on the hidden field to make the Devise works properly after checking the OTP. In our strategy, this is needed. Please check the codes on step 4 for details.
Conclusion
- Implement two-factor authentication for Rails app by using Devise gem, Google authenticator, and ActiveModel::Otp gem isn't easy but it's doable.
- The most challenging part is the strategy to integrate the ActiveModel::Otp gem with Devise gem.