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 of authentication.
For this tutorial I am going to show you how to add two factor authentication to your Rails site using the Nexmo Verify API. For this purpose I have built a little app 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.
# ensure you have Ruby and Bundler installed
git clone https://github.com/nexmo-community/nexmo-rails-2fa-demo.git
cd nexmo-rails-2fa-demo
bundle install
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
device-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 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:
phone_number
to a User
accountLet's start by adding a phone number to a user. I generate a new database migration and change it to add a new column to our user model.
rails generate migration add_phone_number_to_users
# db/migrate/...add_phone_number_to_users.rb
class AddPhoneNumberToUsers < ActiveRecord::Migration
def change
add_column :users, :phone_number, :string
end
end
rake db:migrate
Devise comes with the ability to edit a user right out of the box. By default these views are hidden to the developer though, so I need to get a copy of them to make changes to. Devise makes this pretty easy through a Rails generator.
# this is not the default generator but the one needed for the device-bootstrap-templates gem
rails generate devise:views:bootstrap_templates
# use the command below if you did not use the device-bootstrap-templates gem
rails generate devise:views:templates
This will copy a lot of view templates into app/views/devise/
. I deleted most
of them and only kept the one I really needed: registrations/edit.html.erb
.
The only change I need to make to this template is to add a phone number field right after our email field.
<!-- 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.
# app/controllers/application_controller.rb
...
before_filter :configure_permitted_parameters, if: :devise_controller?
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) << :phone_number
end
...
Now that a user can add their phone number to their account I can have them
verify their phone number on login. In order to send a verification message via
Nexmo Verify I am going to have to add the nexmo
gem to my project.
# Gemfile
gem 'nexmo'
gem 'dotenv-rails', groups: [:development, :test]
As you can see I also added the dotenv-rails
gem. This is so that the app can
load my 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.
# .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 I am just adding a before_action
to my
ApplicationController
that checks if the user has two factor authentication
enabled, and if they do makes sure that they are verified before they are
allowed to continue.
# app/controllers/application_controller.rb
before_action :verify_user!, unless: :devise_controller?
def verify_user!
start_verification if requires_verification?
end
I decided to keep the check to see if the user requires verification very
simple by just checking if they have a phone number on file and if a :verified
value on their session hasn't been set yet.
# app/controllers/application_controller.rb
def requires_verification?
session[:verified].nil? && !current_user.phone_number.blank?
end
To start the verification process I call send_verification_request
(API call #1)
on the Nexmo::Client
object. I 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).
# app/controllers/application_controller.rb
def start_verification
result = Nexmo::Client.new.send_verification_request(
number: current_user.phone_number,
brand: "Kittens & Co"
)
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 I pass the verification request the name of my 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 I redirect the user to a page to fill in the code they will receive. Obviously at this state this would fail as I haven't implemented this just yet.
The final step is to confirm the code the user receives on their phone and set them as verified accordingly. For this I am going to have to add a new page.
I'll start with adding the routes and a basic controller.
# config/routes.rb
resources :verifications, only: [:edit, :update]
# app/controllers/verifications_controller.rb
class VerificationsController < ApplicationController
skip_before_action :verify_user!
def edit
end
def update
...
end
end
As you can see I made sure to skip the before_action
I added earlier so that
the browser doesn't end up in an infinite loop of redirects.
When the user lands on the new page they are presented with a simple form to fill in their verification code.
<!-- 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 I am taking the request_id
and code
from the params and pass them to the
check_verification_request
method (API call #2!) to verify them.
# 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 the confirmation comes back successful I set the user's status as verified and redirect them back to the main page. If the code was not successful I 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.
And that's it! I promised you, just 2 API calls.
The Nexmo Verify API has a lot more options than I showed 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 me doing 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 different than me. 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? Leave me your ideas in the comments below.