Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Two-Factor Authentication with Authy, Ruby and Sinatra


(warning)

Warning

As of November 2022, Twilio no longer provides support for Authy SMS/Voice-only customers. Customers who were also using Authy TOTP or Push prior to March 1, 2023 are still supported. The Authy API is now closed to new customers and will be fully deprecated in the future.

For new development, we encourage you to use the Verify v2 API.

Existing customers will not be impacted at this time until Authy API has reached End of Life. For more information about migration, see Migrating from Authy to Verify for SMS(link takes you to an external page).

This Sinatra(link takes you to an external page) sample application is an example of typical login flow. To run this sample app yourself, download the code and follow the instructions on GitHub(link takes you to an external page).

Adding two-factor authentication (2FA) to your web application increases the security of your user's data. Multi-factor authentication(link takes you to an external page) determines the identity of a user by validating first by logging into the app, and second by validating their mobile device.

For the second factor, we will validate that the user has their mobile phone by either:

  • Sending them a OneTouch push notification to their mobile Authy app or
  • Sending them a token through their mobile Authy app or
  • Sending them a one-time token via text message sent with Authy via Twilio(link takes you to an external page) .

See how VMware uses Authy 2FA to secure their enterprise mobility management solution.(link takes you to an external page)


Configure Authy

configure-authy page anchor

If you haven't configured Authy already now is the time to sign up for Authy(link takes you to an external page). Create your first application naming it as you wish. After you create your application, your "production" API key will be visible on your dashboard(link takes you to an external page).

Once we have an Authy API key we register it as an environment variable.

Configure Authy

configure-authy-1 page anchor

routes/signup.rb


_33
module Routes
_33
module Signup
_33
def self.registered(app)
_33
app.get '/signup' do
_33
haml :signup
_33
end
_33
_33
app.post '/signup' do
_33
password_salt, password_hash = hash_password(params[:password])
_33
user = User.create!(
_33
username: params[:username],
_33
email: params[:email],
_33
password_salt: password_salt,
_33
password_hash: password_hash,
_33
country_code: params[:country_code],
_33
phone_number: params[:phone_number]
_33
)
_33
_33
Authy.api_key = ENV['AUTHY_API_KEY']
_33
authy = Authy::API.register_user(
_33
email: user.email,
_33
cellphone: user.phone_number,
_33
country_code: user.country_code
_33
)
_33
_33
user.update!(authy_id: authy.id)
_33
init_session!(user.id)
_33
_33
redirect '/protected'
_33
end
_33
end
_33
end
_33
end

Let's take a look at how we register a user with Authy.


Register a User with Authy

register-a-user-with-authy page anchor

When a new user signs up for our website, we will call this route. This will store our new user into the database and will register the user with Authy.

All Authy needs to get a user set up for your application is the email, phone number and country code. In order to do a two-factor authentication, we need to make sure we ask for this information at sign up.

Once we register the user with Authy we get an authy_id back. This is very important since it's how we will verify the identity of our user with Authy.

routes/signup.rb


_33
module Routes
_33
module Signup
_33
def self.registered(app)
_33
app.get '/signup' do
_33
haml :signup
_33
end
_33
_33
app.post '/signup' do
_33
password_salt, password_hash = hash_password(params[:password])
_33
user = User.create!(
_33
username: params[:username],
_33
email: params[:email],
_33
password_salt: password_salt,
_33
password_hash: password_hash,
_33
country_code: params[:country_code],
_33
phone_number: params[:phone_number]
_33
)
_33
_33
Authy.api_key = ENV['AUTHY_API_KEY']
_33
authy = Authy::API.register_user(
_33
email: user.email,
_33
cellphone: user.phone_number,
_33
country_code: user.country_code
_33
)
_33
_33
user.update!(authy_id: authy.id)
_33
init_session!(user.id)
_33
_33
redirect '/protected'
_33
end
_33
end
_33
end
_33
end

Having registered our user with Authy, we then can use Authy's OneTouch feature to log them in.


Log in with Authy OneTouch

log-in-with-authy-onetouch page anchor

When a User attempts to log in to our website, we will ask them for a second form of authentication. Let's take a look at OneTouch verification first.

OneTouch works like this:

  • We attempt to send a OneTouch Approval Request to the user.
  • If the user has OneTouch enabled, we will get a success message back.
  • The user hits Approve in their Authy app.
  • Authy makes a POST request to our app with an approved status.
  • We log the user in.

routes/sessions.rb


_48
require 'haml'
_48
_48
module Routes
_48
module Sessions
_48
def self.registered(app)
_48
app.get '/login' do
_48
haml(:login)
_48
end
_48
_48
app.post '/login' do
_48
user = User.first(email: params[:email])
_48
if user && valid_password?(params[:password], user.password_hash)
_48
Authy.api_key = ENV['AUTHY_API_KEY']
_48
user_status = Authy::API.user_status(id: user.authy_id)
_48
puts user_status
_48
required_devices = ['iphone', 'android']
_48
registered_devices = user_status['status']['devices']
_48
_48
if user_status['status']['registered'] \
_48
and (required_devices & registered_devices)
_48
Authy::OneTouch.send_approval_request(
_48
id: user.authy_id,
_48
message: 'Request to Login to Twilio demo app',
_48
details: { 'Email Address' => user.email }
_48
)
_48
_48
status = :onetouch
_48
else
_48
Authy::API.request_sms(id: user.authy_id)
_48
status = :sms
_48
end
_48
user.update!(authy_status: status)
_48
_48
pre_init_session!(user.id)
_48
_48
status.to_s
_48
else
_48
'unauthorized'
_48
end
_48
end
_48
_48
app.get '/logout' do
_48
destroy_session!
_48
redirect '/login'
_48
end
_48
end
_48
end
_48
end

In the next steps we'll look at how we handle cases where the user does not have OneTouch, or denies the login request.


Send the OneTouch Request

send-the-onetouch-request page anchor

When our user logs in we immediately attempt to verify their identity with OneTouch. We will fallback gracefully if they don't have a OneTouch device, but we don't know until we try.

Authy allows us to input details with our OneTouch request, including a message, a logo and so on. We could easily send any amount of details by appending details['some_detail']. You could imagine a scenario where we send a OneTouch request to approve a money transfer.


_10
"message" => "Request to Send Money to Jarod's vault",
_10
"details['Request From']" => "Jarod",
_10
"details['Amount Request']" => "1,000,000",
_10
"details['Currency']" => "Galleons",

Once we send the request we need to update our user's authy_status based on the response.

Implement OneTouch Approval

implement-onetouch-approval page anchor

routes/sessions.rb


_48
require 'haml'
_48
_48
module Routes
_48
module Sessions
_48
def self.registered(app)
_48
app.get '/login' do
_48
haml(:login)
_48
end
_48
_48
app.post '/login' do
_48
user = User.first(email: params[:email])
_48
if user && valid_password?(params[:password], user.password_hash)
_48
Authy.api_key = ENV['AUTHY_API_KEY']
_48
user_status = Authy::API.user_status(id: user.authy_id)
_48
puts user_status
_48
required_devices = ['iphone', 'android']
_48
registered_devices = user_status['status']['devices']
_48
_48
if user_status['status']['registered'] \
_48
and (required_devices & registered_devices)
_48
Authy::OneTouch.send_approval_request(
_48
id: user.authy_id,
_48
message: 'Request to Login to Twilio demo app',
_48
details: { 'Email Address' => user.email }
_48
)
_48
_48
status = :onetouch
_48
else
_48
Authy::API.request_sms(id: user.authy_id)
_48
status = :sms
_48
end
_48
user.update!(authy_status: status)
_48
_48
pre_init_session!(user.id)
_48
_48
status.to_s
_48
else
_48
'unauthorized'
_48
end
_48
end
_48
_48
app.get '/logout' do
_48
destroy_session!
_48
redirect '/login'
_48
end
_48
end
_48
end
_48
end

Once we send the request we need to update our user's AuthyStatus based on the response. But first we have to register a OneTouch callback endpoint.


Configure the OneTouch callback

configure-the-onetouch-callback page anchor

In order for our app to know what the user did after we sent the OneTouch request, we need to register a callback endpoint with Authy.

Note: In order to verify that the request is coming from Authy, we've written the helper method authenticate_request! that will halt the request if it appears it isn't coming from Authy.

Here in our callback, we look up the user using the Authy ID sent with the Authy POST request. Ideally at this point we would probably use a websocket to let our client know that we received a response from Authy. However for this version we're going to keep it simple and just update the authy_status on the user.

Configure the OneTouch callback

configure-the-onetouch-callback-1 page anchor

routes/confirmation.rb


_62
module Routes
_62
module Confirmation
_62
def self.registered(app)
_62
app.post '/authy/callback' do
_62
authenticate_request!(request)
_62
_62
request.body.rewind
_62
params = JSON.parse(request.body.read)
_62
authy_id = params['authy_id']
_62
authy_status = params['status']
_62
_62
begin
_62
user = User.first(authy_id: authy_id)
_62
user.update!(authy_status: authy_status)
_62
rescue => e
_62
puts e.message
_62
end
_62
_62
'OK'
_62
end
_62
_62
app.post '/authy/status' do
_62
user = User.first(id: current_user)
_62
user.authy_status.to_s
_62
end
_62
_62
app.post '/confirm-login' do
_62
user = User.first(id: current_user)
_62
authy_status = user.authy_status
_62
_62
user.update(authy_status: :unverified)
_62
_62
if authy_status == :approved
_62
init_session!(user.id)
_62
redirect '/protected'
_62
else
_62
destroy_session!
_62
redirect '/login'
_62
end
_62
end
_62
_62
app.post '/send-token' do
_62
user = User.first(id: current_user)
_62
Authy::API.request_sms(id: user.authy_id)
_62
'Token has been sent'
_62
end
_62
_62
app.post '/verify-token' do
_62
user = User.first(id: current_user)
_62
token = Authy::API.verify(id: user.authy_id, token: params[:token])
_62
if token.ok?
_62
init_session!(user.id)
_62
redirect '/protected'
_62
else
_62
# 'Incorrect code, please try again'
_62
destroy_session!
_62
redirect '/login'
_62
end
_62
end
_62
end
_62
end
_62
end

Our application is now capable of using Authy for two-factor authentication. However, we are still missing an important part: the client-side code that will handle it.


Disabling Unsuccessful Callbacks

disabling-unsuccessful-callbacks page anchor

Scenario: The OneTouch callback URL provided by you is no longer active.

Action: We will disable the OneTouch callback after 3 consecutive HTTP error responses. We will also

  • Set the OneTouch callback URL to blank.
  • Send an email notifying you that the OneTouch callback is disabled with details on how to enable the OneTouch callback.

How to enable OneTouch callback? You need to update the OneTouch callback endpoint, which will allow the OneTouch callback.

Visit the Twilio Console: Console > Authy > Applications > {ApplicationName} > Push Authentication > Webhooks > Endpoint/URL to update the Endpoint/URL with a valid OneTouch callback URL.


Handle Two-Factor in the Browser

handle-two-factor-in-the-browser page anchor

We've already taken a look at what's happening on the server side, so let's step in front of the cameras and see how our JavaScript is interacting with those server endpoints.

When we expect a OneTouch response, we will begin by polling /authy/status until we see an Authy status is not empty. Let's take a look at this controller and see what is happening.

Poll the server until we see the result of the Authy OneTouch login

poll-the-server-until-we-see-the-result-of-the-authy-onetouch-login page anchor

public/javascripts/app.js


_56
$(document).ready(function() {
_56
$("#login-form").submit(function(event) {
_56
event.preventDefault();
_56
_56
var data = $(event.currentTarget).serialize();
_56
authyVerification(data);
_56
});
_56
_56
var authyVerification = function (data) {
_56
$.post("/login", data, function (result) {
_56
resultActions[result]();
_56
});
_56
};
_56
_56
var resultActions = {
_56
onetouch: function() {
_56
$("#authy-modal").modal({ backdrop: "static" }, "show");
_56
$(".auth-token").hide();
_56
$(".auth-onetouch").fadeIn();
_56
monitorOneTouchStatus();
_56
},
_56
_56
sms: function () {
_56
$("#authy-modal").modal({ backdrop: "static" }, "show");
_56
$(".auth-onetouch").hide();
_56
$(".auth-token").fadeIn();
_56
requestAuthyToken();
_56
},
_56
_56
unauthorized: function () {
_56
$("#error-message").text("The email and password you entered don't match.");
_56
}
_56
};
_56
_56
var monitorOneTouchStatus = function () {
_56
$.post("/authy/status")
_56
.done(function (data) {
_56
if (data === "approved" || data === "denied") {
_56
$("#confirm-login").submit();
_56
} else {
_56
setTimeout(monitorOneTouchStatus, 2000);
_56
}
_56
});
_56
}
_56
_56
var requestAuthyToken = function () {
_56
$.post("/authy/request-token")
_56
.done(function (data) {
_56
$("#authy-token-label").text(data);
_56
});
_56
}
_56
_56
$("#logout").click(function() {
_56
$("#logout-form").submit();
_56
});
_56
});

Finally, we can confirm the login.


If authy_status is approved the user will be redirected to the protected content, otherwise we'll show the login form with a message that indicates the request was denied.

Redirect user to the right page based based on authentication status

redirect-user-to-the-right-page-based-based-on-authentication-status page anchor

routes/confirmation.rb


_62
module Routes
_62
module Confirmation
_62
def self.registered(app)
_62
app.post '/authy/callback' do
_62
authenticate_request!(request)
_62
_62
request.body.rewind
_62
params = JSON.parse(request.body.read)
_62
authy_id = params['authy_id']
_62
authy_status = params['status']
_62
_62
begin
_62
user = User.first(authy_id: authy_id)
_62
user.update!(authy_status: authy_status)
_62
rescue => e
_62
puts e.message
_62
end
_62
_62
'OK'
_62
end
_62
_62
app.post '/authy/status' do
_62
user = User.first(id: current_user)
_62
user.authy_status.to_s
_62
end
_62
_62
app.post '/confirm-login' do
_62
user = User.first(id: current_user)
_62
authy_status = user.authy_status
_62
_62
user.update(authy_status: :unverified)
_62
_62
if authy_status == :approved
_62
init_session!(user.id)
_62
redirect '/protected'
_62
else
_62
destroy_session!
_62
redirect '/login'
_62
end
_62
end
_62
_62
app.post '/send-token' do
_62
user = User.first(id: current_user)
_62
Authy::API.request_sms(id: user.authy_id)
_62
'Token has been sent'
_62
end
_62
_62
app.post '/verify-token' do
_62
user = User.first(id: current_user)
_62
token = Authy::API.verify(id: user.authy_id, token: params[:token])
_62
if token.ok?
_62
init_session!(user.id)
_62
redirect '/protected'
_62
else
_62
# 'Incorrect code, please try again'
_62
destroy_session!
_62
redirect '/login'
_62
end
_62
end
_62
end
_62
end
_62
end

That's it! We've just implemented two-factor auth using three different methods and the latest Authy technology.


If you're a Ruby developer working with Twilio, you might enjoy these other tutorials:

Click-To-Call

Click-to-call enables your company to convert web traffic into phone calls with the click of a button

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Connect with us on Twitter(link takes you to an external page) and let us know what you build!


Rate this page: