I love applications that support two factor authentication! Whether it is through SMS, voice, or other means – it simply tells me that the app developer has been kind enough to think about my data and its security.
In 2015 more than 150 million user records were stolen and the leaked data proved that people still tend to use the same passwords across different sites. For those people the only thing standing between them and another compromised account is a good second factor authentication.
A little business app
For this tutorial I am going to show you how to add two factor authentication to your Rails app using the Nexmo Verify API. For this purpose I have built a little site called “Kittens & Co” – a social network where business cats can exchange their plans to take over the world.
You can download the starting point of the app from Github and run it locally.
1 2 3 4 5 6 7 |
# ensure you have Ruby and Bundler installed git clone https://github.com/nexmo-community/nexmo-rails-devise-2fa-demo.git cd nexmo-rails-devise-2fa-demo bundle install rake db:migrate RAILS_ENV=development rails server |
Then visit 127.0.0.1:3000 in your browser and register.
By default the app implements registration and login using Devise but most of this tutorial applies similarly to apps that use other authentication methods. Additionally I added the bootstrap-sass
and devise-bootstrap-templates
gems for some prettyfication of our app.
All the code for this starting point can be found on the basic-login branch on Github. All the code I will be adding below can be found on the two-factor branch. For your convenience you can see all the changes between our start and end point on Github as well.
Nexmo Verify for 2FA
Nexmo Verify is phone verification made simple. Most two factor authentication plugins will require you to manage your own tokens, token expiry, retries, and SMS sending. Verify manages all of this for you and all you need to know are just 2 API calls!
To add Nexmo Verify to our system I am going to add the following changes:
- Add a
phone_number
to aUser
account - Require verification on login if the user has a number on their account
- Verify the code sent to their number and log the user in
Adding a phone number
Let’s start by adding a phone number to a user. We’ll do this by generating a new database
migration.
1 2 |
rails generate migration add_phone_number_to_users |
And then change it to add a new column to our user model.
1 2 3 4 5 6 7 |
# db/migrate/...add_phone_number_to_users.rb class AddPhoneNumberToUsers < ActiveRecord::Migration def change add_column :users, :phone_number, :string end end |
1 2 |
rake db:migrate |
Devise comes with the ability to edit a user right out of the box. By default these views are hidden, so we need to get a copy of them to make changes to. Devise makes this pretty easy through a Rails generator.
1 2 3 4 5 6 |
# this is not the default generator but the one needed for the devise-bootstrap-templates gem rails generate devise:views:bootstrap_templates # uncomment and run the command below if you did not use the devise-bootstrap-templates gem # rails generate devise:views:templates |
This will copy a lot of view templates into app/views/devise/
. We need to delete most of them and only kept the one we really needed: registrations/edit.html.erb
.
The only change we need to make to this template is to add a phone number field right after our email field.
1 2 3 4 5 6 |
<!-- app/views/devise/registrations/edit.html.erb --> <div class="form-group"> <%= f.label :phone_number %> <i>(Leave blank to disable two factor authentication)</i><br /> <%= f.number_field :phone_number, class: "form-control", placeholder: "e.g. 447555555555 or 1234234234234" %> </div> |
The last step is to make Devise aware of this extra parameter. Without these lines the phone number won’t be accepted as a strong parameter and will be lost after it’s submitted.
1 2 3 4 5 6 7 8 9 |
# app/controllers/application_controller.rb ... before_filter :configure_permitted_parameters, if: :devise_controller? def configure_permitted_parameters devise_parameter_sanitizer.permit(:account_update, keys: [:phone_number]) end ... |
To test this out, navigate to http://localhost:3000/users/sign_up, create an account, click on your email on the top right of the screen, enter your phone number and the password you used at signup and click Update. This will save your phone number to the database.
Send a 2FA verification request
Now that a user can add their phone number to their account we can have them verify their phone number on login. In order to send a verification message via Nexmo Verify we’re going to have to add the nexmo
gem to our project.
1 2 3 4 |
# Gemfile gem 'nexmo' gem 'dotenv-rails', groups: [:development, :test] |
1 2 |
bundle install |
As you can see we also need to add the dotenv-rails
gem. This is so that the app can load our API credentials from a .env
file. The Nexmo gem automatically picks up those environment variables and uses them to initialize the client. You can find your credentials on the settings page of your Nexmo account.
1 2 3 4 |
# .env NEXMO_API_KEY='your_key' NEXMO_API_SECRET='your_secret' |
There are many different ways you could implement the verification check in your app. To keep things simple let’s add a before_action
to our ApplicationController
that checks if the user has two factor authentication enabled. If they do, make sure that they are verified before they are allowed to continue.
1 2 3 4 5 6 7 |
# app/controllers/application_controller.rb before_action :verify_user!, unless: :devise_controller? def verify_user! start_verification if requires_verification? end |
Let’s keep the code to see if the user requires verification very simple by checking if they have a phone number on file and if a :verified
value on their session hasn’t been set yet.
1 2 3 4 5 |
# app/controllers/application_controller.rb def requires_verification? session[:verified].nil? && !current_user.phone_number.blank? end |
To start the verification process, call send_verification_request
(API call #1) on the Nexmo::Client
object. We don’t need to pass in any API credentials because it has already been initialized through our environment values – though if you want to be explicit you can (see the gem documentation).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# app/controllers/application_controller.rb def start_verification result = Nexmo::Client.new.send_verification_request( number: current_user.phone_number, brand: "Kittens and Co", sender_id: 'Kittens' ) if result['status'] == '0' redirect_to edit_verification_path(id: result['request_id']) else sign_out current_user redirect_to :new_user_session, flash: { error: 'Could not verify your number. Please contact support.' } end end |
As you can see we pass the verification request the name of the web app. This is used in the text message the user receives and adds some very nice brand personalisation.
If the message has been sent successfully we redirect the user to a page to fill in the code they will receive. Obviously at this stage this would fail because we haven’t implemented this just yet.
Check 2FA verification code
The final step is to confirm the code the user receives on their phone and set them as verified accordingly. For this we’re going to have to add a new page.
Let’s start with adding the routes.
1 2 3 |
# config/routes.rb resources :verifications, only: [:edit, :update] |
And also create a basic controller.
1 2 3 4 5 6 7 8 9 10 11 12 |
# app/controllers/verifications_controller.rb class VerificationsController < ApplicationController skip_before_action :verify_user! def edit end def update ... end end |
As you can see we’ve made sure to skip the before_action
we added to the ApplicationController
earlier so that the browser doesn’t end up in an infinite loop of redirects.
Next, we should create a view so that when the user lands on the new page they are presented with a simple form to fill in their verification code.
1 2 3 4 5 6 7 8 9 |
<!-- app/views/verifications/edit.html.erb --> <%= form_tag verification_path(id: params[:id]), method: :put do %> <div class="form-group"> <%= label_tag :code %><br /> <%= number_field_tag :code, class: "form-control" %> </div> <%= submit_tag 'Verify', class: "btn btn-primary" %> <% end %> |
The user then submits their code to the new update
action. In this action we take the request_id
and code
from the params and pass them to the check_verification_request
method (API call #2!) to verify them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# app/controllers/verifications_controller.rb def update confirmation = Nexmo::Client.new.check_verification_request( request_id: params[:id], code: params[:code] ) if confirmation['status'] == '0' session[:verified] = true redirect_to :root, flash: { success: 'Welcome back.' } else redirect_to edit_verification_path(id: params[:id]), flash: { error: confirmation['error_text'] } end end |
When a successful confirmation comes back we set the user’s status as verified and redirect them back to the main page. If the code was not successful we instead present the user with a message describing what went wrong. A full list of the response status codes can be found in the Verify documentation.
If you run the app, logout (if required), and login you’ll now be presented with the verify form.
And that’s it! I promised you, just 2 API calls.
Next steps
The Nexmo Verify API has a lot more options than we’ve covered here, ranging from searching and controlling the requests, to changing the code length and expiry time. Although the code I showed here is pretty simple I ended up with a very powerful out of the box experience. The system falls back to phone calls if needed, expires tokens without you having to do anything, prevents reuse of tokens, and logs verification times.
The Nexmo Ruby library is very agnostic as to how it’s used which means you could implement things very differently than I did here. For example, you could require the phone number on user registration, rejecting new accounts until the number has been validated.
I’d love to know what you’d add next? Please drop me a tweet (I’m @cbetta) with thoughts and ideas.
