SMS Two-Factor Authentication with Ruby and Sinatra

Download the Code

This Sinatra application example demonstrates how to implement an SMS two-factor authentication using Twilio.

Adding two-factor authentication (2FA) to your web application increases the security of your user's data. Multi-factor authentication determines the identity of a user in two steps:

  1. First we validate the user with an email and password.
  2. Second, we validate using a mobile device, by sending them a one-time verification code.

Once our user enters the verification code we can validate the ownership of the account. This is a standard SMS implementation.

Loading Code Samples...
Language
module VerificationSender
  def self.send_verification_to(user)
    verification_code = CodeGenerator.generate
    user.update(verification_code: verification_code)
    MessageSender.send_code(user.phone_number, verification_code)
  end
end
lib/verification_sender.rb
Send verification code

lib/verification_sender.rb

Generate a Verification Code

Once our user logs in we need to send him/her a verification code.

To generate our verification code we use Random#rand which can take a range as an argument. Considering the current implementation our 6-digit verification code could be any number between 100000 and 999999.

Loading Code Samples...
Language
module CodeGenerator
  def self.generate
    rand(100000...999999).to_s
  end
end
lib/code_generator.rb
Generate a Verification Code

lib/code_generator.rb

Next let's look at how we would send this in an SMS with Twilio.

Send a Verification Code

The Twilio helper library allows us to easily send an SMS.

First we have to create an instance of a Twilio Client with our credentials. Now all we have to do to send an SMS using the REST API is to call client.messages.create() with the necessary parameters.

You can find your credentials at your Twilio Account.

Loading Code Samples...
Language
module MessageSender
  def self.send_code(phone_number, code)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    client = Twilio::REST::Client.new(account_sid, auth_token)

    message = client.messages.create(
      from:  ENV['TWILIO_NUMBER'],
      to:    phone_number,
      body:  code
    )

    message.status == 'queued'
  end
end
lib/message_sender.rb
Send a Verification Code

lib/message_sender.rb

We've seen how we will use the Twilio Client to send the SMS verification code. Now let's see how our controller will use this utility.

Register a User

When a user signs up for our website, this controller creates the user and sends him/her a verification code.

In order to do two-factor authentication we need to make sure we ask for the user's phone number.

Let's see how we implemented the send_verification_to method.

Loading Code Samples...
Language
module Routes
  module Users
    def self.registered(app)
      app.get '/users/new' do
        @user = User.new
        haml :'users/new'
      end

      app.post '/users' do
        user_params = {
          first_name:   params[:first_name],
          last_name:    params[:last_name],
          email:        params[:email],
          phone_number: params[:phone_number],
          password:     params[:password]
        }

        @user = User.new(user_params)
        if @user.save
          session[:user_id] = @user.id
          VerificationSender.send_verification_to(@user)
          redirect '/confirmations/new'
        else
          flash.now[:error] = @user.errors.full_messages.join(", ")
          haml :'users/new'
        end
      end
    end
  end
end
routes/users.rb
Register a User

routes/users.rb

Now let's take a closer at how to proceed with the 2-step verification.

Putting It All Together

Using the building blocks we created in the previous steps we can put it all together.

Notice we updated the user with the verification code since we need to look it up to verify the user.

Loading Code Samples...
Language
module VerificationSender
  def self.send_verification_to(user)
    verification_code = CodeGenerator.generate
    user.update(verification_code: verification_code)
    MessageSender.send_code(user.phone_number, verification_code)
  end
end
lib/verification_sender.rb
Send the confirmation code and save in the session

lib/verification_sender.rb

And now, a drumroll for the second step of the two-step authentication implementation...

Implement the 2-Step Verification

When the user receives an SMS with the verification code we need to ensure the given code is valid.

This validation is achieved by comparing the verification code sent to the user with the one the user inputs on the form.

Confirm Verification Code

If the validation was successful the application allows the user to have access to the protected content. Otherwise the application will prompt for the verification code once again.

Loading Code Samples...
Language
module Routes
  module Confirmations
    def self.registered(app)
      app.get '/confirmations/new' do
        @user = User.get(session[:user_id])
        haml :'confirmations/new'
      end

      app.post '/confirmations' do
        @user = User.get(params[:user_id])
        if @user.verification_code == params[:verification_code]
          @user.update(confirmed: true)

          session[:authenticated] = true

          flash[:notice] = "Welcome #{@user.first_name}. The Adventure Begins!"
          redirect '/protected'
        else
          flash.now[:error] = "Verification code is incorrect."
          haml :'confirmations/new'
        end
      end
    end
  end
end
routes/confirmations.rb
Implement the 2-Step Verification

routes/confirmations.rb

That's it! We've just implemented SMS Two-Factor Authentication that you can now use in your applications!

Where to next?

If you're a Ruby developer working with Twilio, you might want to check out these other tutorials.

SMS and MMS Notifications

Faster than e-mail and less likely to get blocked, text messages are great for timely alerts and notifications

Automated Survey

Instantly collect structured data from your users with a survey conducted over a call or an SMS text messages.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!

Agustin Camino
Hector Ortega
David Prothero
Andrew Baker
Jose Oliveros

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
module VerificationSender
  def self.send_verification_to(user)
    verification_code = CodeGenerator.generate
    user.update(verification_code: verification_code)
    MessageSender.send_code(user.phone_number, verification_code)
  end
end
module CodeGenerator
  def self.generate
    rand(100000...999999).to_s
  end
end
module MessageSender
  def self.send_code(phone_number, code)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    client = Twilio::REST::Client.new(account_sid, auth_token)

    message = client.messages.create(
      from:  ENV['TWILIO_NUMBER'],
      to:    phone_number,
      body:  code
    )

    message.status == 'queued'
  end
end
module Routes
  module Users
    def self.registered(app)
      app.get '/users/new' do
        @user = User.new
        haml :'users/new'
      end

      app.post '/users' do
        user_params = {
          first_name:   params[:first_name],
          last_name:    params[:last_name],
          email:        params[:email],
          phone_number: params[:phone_number],
          password:     params[:password]
        }

        @user = User.new(user_params)
        if @user.save
          session[:user_id] = @user.id
          VerificationSender.send_verification_to(@user)
          redirect '/confirmations/new'
        else
          flash.now[:error] = @user.errors.full_messages.join(", ")
          haml :'users/new'
        end
      end
    end
  end
end
module VerificationSender
  def self.send_verification_to(user)
    verification_code = CodeGenerator.generate
    user.update(verification_code: verification_code)
    MessageSender.send_code(user.phone_number, verification_code)
  end
end
module Routes
  module Confirmations
    def self.registered(app)
      app.get '/confirmations/new' do
        @user = User.get(session[:user_id])
        haml :'confirmations/new'
      end

      app.post '/confirmations' do
        @user = User.get(params[:user_id])
        if @user.verification_code == params[:verification_code]
          @user.update(confirmed: true)

          session[:authenticated] = true

          flash[:notice] = "Welcome #{@user.first_name}. The Adventure Begins!"
          redirect '/protected'
        else
          flash.now[:error] = "Verification code is incorrect."
          haml :'confirmations/new'
        end
      end
    end
  end
end