Implementing SMS two-factor authentication (2FA) with MessageBird

⏱ 15 min build time || Download the Code

Why build two-factor authentication?

In this MessageBird Developer Tutorial you’ll learn how to improve your security building an SMS-based two factor authentication solution with the MessageBird Verify API. The runnable application we’ll build is a prototype in Ruby for our fictitious online banking application, BirdBank.

Enterprises are increasingly challenged to keep sensitive information from falling into the wrong hands. This means that we can no longer trust old online authentication systems that rely solely on usernames and passwords, especially as security breaches grow in frequency, severity and sophistication.

With the MessageBird Verify API, you can implement two factor authentication (2FA) solutions to provide an additional layer of account security by verifying the user's password with a second authentication token and in turn, secure customer data, block fraudulent accounts, and safeguard key transactions in a matter of minutes. The most common use case involves the application of one-time passwords (OTP) generated by hardware tokens, authenticator apps or directly sent to the user's mobile phone via SMS messaging.

We'll walk you through the following steps:

  • Asking for the phone number
  • Sending a verification code
  • Verifying the code

Pro-tip: Follow this tutorial to build the whole application from scratch or, if you want to see it in action right away, you can download, clone or fork the sample application from our MessageBird Developer Tutorials GitHub repository.

Getting started

To build our sample application we'll use Ruby, the Sinatra framework and the ERB templates as well as the MessageBird SDK.

Before we get started, make sure that Ruby's package manager, bundler, is installed. If not, you can easily install it for free.

Project setup

Dependencies

First, create a new directory to store the sample application. Within this new directory, create a file called Gemfile with the following contents:

source 'http://rubygems.org'
gem 'dotenv', '~> 2.5'
gem 'messagebird-rest', '~> 1.4', require: 'messagebird'
gem 'sinatra', '~> 2.0'

This file defines all of the dependencies necessary for your sample application, including Sinatra and the MessageBird SDK; the ERB templating system comes along with Ruby.

Next, we need to instruct bundler to install all the required dependencies in your project. Open a console pointed to the directory that contains the file you just created and type the following command:

bundle install

Create your API Key 🔑

To enable the MessageBird REST API, we need to provide an access key for the API. MessageBird provides keys in live and test modes. To get this application running, we’ll need to create and use a live API access key. You can read more about the difference between test and live API keys in our Help Center.

Let's create your live API access key. First, go to the MessageBird Dashboard; if you have already created an API key it will be shown right there. If you don’t see any key on the Dashboard or if you're unsure whether this key is in live mode, go to the Developers section in the MessageBird Dashboard and open the API access (REST) tab. There you can create new API keys and manage your existing ones.

If you are having any issues creating your API key, please reach out to support@messagebird.com; we’ll make sure to help you out.

Pro-tip: Hardcoding your credentials in the code is a risky practice that should never be used in production applications. A better method, also recommended by the Twelve-Factor App Definition, is to use environment variables.

We've added dotenv to the sample application, so you can supply your API key in a file named .env. Simply copy the provided file env.example to .env and add your API key like this:

MESSAGEBIRD_API_KEY=YOUR-API-KEY

Main file

You now have your API key, so let's get started with the main file. First, create an app.rb file in the same directory as your Gemfile. This file starts off by requiring dependencies:

require 'dotenv'
require 'sinatra'
require 'messagebird'

Then, we'll set up a source for our view templates:

set :root, File.dirname(__FILE__)

Then, we'll load any required environment variables:

Dotenv.load if Sinatra::Base.development?
client = MessageBird::Client.new(ENV['MESSAGEBIRD_API_KEY'])

Next, we're creating a new MessageBird client. This will be used to access the API.

And finally, we’ll set up an endpoint which we can access:

get '/' do
'Hello!'
end

If you run ruby app.rb, you should see a message similar to the following:

== Sinatra (v2.0.4) has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.7.2 codename Bachmanity)
Maximum connections set to 1024
Listening on localhost:4567, CTRL+C to stop

Head to localhost:4567 in your browser, and you should be greeted with a message! This shows that the basics of the app are working. 🤓

Views

We use ERB to separate the logic of our code from the HTML pages. To do this, create a directory named views. Now, inside views create a file called layout.erb inside with the following content:

<!doctype html>
<html>
<head>
<title>MessageBird Verify Example</title>
</head>
<body>
<h1>MessageBird Verify Example</h1>
<%= yield %>
</body>
</html>

This is the main layout which acts as a container for all pages of our application. We'll create the views for each page next.

Asking for the phone number

The first step in verifying a user's phone number is asking them to provide their phone number. Let's do exactly this by creating an HTML form and storing it as step1.erb inside the views directory:

<% if !errors.nil? %>
<p><%= errors %></p>
<% end %>
<p>Please enter your phone number (in international format, starting with +) to receive a verification code:</p>
<form method="post" action="/step2">
<input type="tel" name="number" />
<input type="submit" value="Send code" />
</form>

The form is simple, having just one input field and one submit button. Providing tel as the type attribute of our input field allows some browsers, especially on mobile devices, to optimize for telephone number input, for example by displaying a number pad. The section starting with <% if error %> is needed to display errors—we'll come back to this in a minute.

Now, it's time to change the initial route in app.rb to display the page:

get '/' do
erb :step1, locals: { errors: nil }
end

Running ruby app.rb should display the form.

Sending a verification code

Once we've collected the number, we can send a verification message to a user's mobile device. The MessageBird Verify API takes care of generating a random token, so you don't have to do this yourself. Codes are numeric and six digits by default. If you want to customize the length of the code or configure other options, you can check out our Verify API documentation.

The form we created in the last step submits the phone number via HTTP POST to /step2, so let's define this route in our app.rb:

post '/step2' do
number = params['number']
otp = nil
begin
otp = client.verify_create(
number,
originator: 'Code',
template: 'Your verification code is %token.'
)
rescue MessageBird::ErrorException => ex
errors = ex.errors.each_with_object([]) do |error, memo|
memo << "Error code #{error.code}: #{error.description}"
end.join("\n")
return erb :step1, locals: { errors: errors }
end
erb :step2, locals: { otp_id: otp.id, errors: nil }
end

Before we move on, let's quickly dive into what happens here: 🤔

First, the number is passed along from the request. The MessageBird client calls verify_create() with the number as the first parameter. The second parameter is used to provide additional options:

  • The originator is the sender for the message; it can be a telephone number including country code or an alphanumeric string (with a maximum length of 11 characters). You can use the number you bought as part of our GGetting Started tutorial as originator. Keep in mind that alphanumeric senders are not supported in every country including the United States, so it’s important to check the country restrictions. If you can't use alphanumeric IDs, use a real phone number instead. You can check our originator article in Help Center to learn more about this topic.

  • Using template we can phrase the wording of the message. The template contains the placeholder %token, which is replaced with the generated token on MessageBird's end. If we omitted this, the message would simply contain the token and nothing else.

If this call fails, the MessageBird client throws MessageBird::ErrorException error. A typical error could be that the user has entered an invalid phone number. For our application, we simply re-render the page from our first step and pass the description of the error into the template - remember the <%= if !errors.nil %> section from the first step? In production applications, you'd most likely not expose the raw API error; instead, you could consider different possible problems and return an appropriate message in your own words. You might also want to prevent some errors from occurring by doing some input validation on the phone number yourself.

In case the request was successful, we'll render a new page. Our API response contains an ID, which we'll need for the next step, so we'll just add it to the form. Since the ID is meaningless without your API access key there are no security implications of doing so; however, in practice you'd be more likely to store this ID in a session object on the server. Just as before, we're logging the whole response to the console for debugging purposes. We still need to build the new page, so create a file called step2.erb in your views directory:

<% if !errors.nil? %>
<p><%= errors %></p>
<% end %>
<p>We have sent you a verification code!</p>
<p>Please enter the code here:</p>
<form method="post" action="/step3">
<input type="hidden" name="id" value="<%= otp_id %>" />
<input type="text" name="token" />
<input type="submit" value="Check code" />
</form>

The form is very similar to the first step. Keep in mind that we include a hidden field with our verification ID and once again have a conditional error section.

Verifying the code

The user will check their phone and enter the code into our form. What we need to do next is send the user's input along with the ID of the verification request to MessageBird's API and see whether the verification was successful or not. Let's declare this third step as a new route in our app.rb:

post '/step3' do
id = params[:id]
token = params[:token]
begin
client.verify_token(id, token)
rescue MessageBird::ErrorException => ex
errors = ex.errors.each_with_object([]) do |error, memo|
memo << "Error code #{error.code}: #{error.description}"
end.join("\n")
return erb :step2, locals: { otp_id: nil, errors: errors }
end
erb :step3, locals: { errors: nil }
end

This code looks very similar to the one in the second step. First, we're reading the input and then make a call to MessageBird's API. This time, it's the verify_token() method, which accepts id and token as its parameters.

In case of an error, such as an invalid or expired token, we're showing that error on our page from the second step.

In the success case, we simply show a new page. Create this page in your views directory and call it step3.erb:

<p>You have successfully verified your phone number.</p>

Testing

You're done! To check if your application works run it one more time from the command line:

ruby app.rb

Then, point your browser to http://localhost:4567/ and try to verify your own phone number.

Awesome! You can now leverage the flow, code snippets and UI examples from this tutorial to build your own two factor authentication system. Don't forget to download the code from the MessageBird Developer Tutorials GitHub repository.

Nice work! 🎉

You now have a running integration of MessageBird's Verify API with Ruby!

Start building!

Want to build something similar but not quite sure how to get started? Please feel free to let us know at support@messagebird.com; we'd love to help!

Questions?

We’re always happy to help with code or other doubts you might have! Check out our Quickstarts, API Reference, Tutorials, SDKs, or contact our Support team.

Cookie Settings