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.
This Sinatra sample application is an example of typical login flow. To run this sample app yourself, download the code and follow the instructions on GitHub.
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 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:
See how VMware uses Authy 2FA to secure their enterprise mobility management solution.
If you haven't configured Authy already now is the time to sign up for Authy. Create your first application naming it as you wish. After you create your application, your "production" API key will be visible on your dashboard.
Once we have an Authy API key we register it as an environment variable.
routes/signup.rb
_33module 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_33end
Let's take a look at how we register a user with Authy.
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
_33module 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_33end
Having registered our user with Authy, we then can use Authy's OneTouch feature to log them in.
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:
success
message back.
approved
status.
routes/sessions.rb
_48require 'haml'_48_48module 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_48end
In the next steps we'll look at how we handle cases where the user does not have OneTouch, or denies the login request.
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.
routes/sessions.rb
_48require 'haml'_48_48module 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_48end
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.
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.
routes/confirmation.rb
_62module 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_62end
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.
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
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.
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.
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.
routes/confirmation.rb
_62module 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_62end
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 enables your company to convert web traffic into phone calls with the click of a button
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 and let us know what you build!