Skip to contentSkip to navigationSkip to topbar
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

1
module Routes
2
module Signup
3
def self.registered(app)
4
app.get '/signup' do
5
haml :signup
6
end
7
8
app.post '/signup' do
9
password_salt, password_hash = hash_password(params[:password])
10
user = User.create!(
11
username: params[:username],
12
email: params[:email],
13
password_salt: password_salt,
14
password_hash: password_hash,
15
country_code: params[:country_code],
16
phone_number: params[:phone_number]
17
)
18
19
Authy.api_key = ENV['AUTHY_API_KEY']
20
authy = Authy::API.register_user(
21
email: user.email,
22
cellphone: user.phone_number,
23
country_code: user.country_code
24
)
25
26
user.update!(authy_id: authy.id)
27
init_session!(user.id)
28
29
redirect '/protected'
30
end
31
end
32
end
33
end
34

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

1
module Routes
2
module Signup
3
def self.registered(app)
4
app.get '/signup' do
5
haml :signup
6
end
7
8
app.post '/signup' do
9
password_salt, password_hash = hash_password(params[:password])
10
user = User.create!(
11
username: params[:username],
12
email: params[:email],
13
password_salt: password_salt,
14
password_hash: password_hash,
15
country_code: params[:country_code],
16
phone_number: params[:phone_number]
17
)
18
19
Authy.api_key = ENV['AUTHY_API_KEY']
20
authy = Authy::API.register_user(
21
email: user.email,
22
cellphone: user.phone_number,
23
country_code: user.country_code
24
)
25
26
user.update!(authy_id: authy.id)
27
init_session!(user.id)
28
29
redirect '/protected'
30
end
31
end
32
end
33
end
34

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

1
require 'haml'
2
3
module Routes
4
module Sessions
5
def self.registered(app)
6
app.get '/login' do
7
haml(:login)
8
end
9
10
app.post '/login' do
11
user = User.first(email: params[:email])
12
if user && valid_password?(params[:password], user.password_hash)
13
Authy.api_key = ENV['AUTHY_API_KEY']
14
user_status = Authy::API.user_status(id: user.authy_id)
15
puts user_status
16
required_devices = ['iphone', 'android']
17
registered_devices = user_status['status']['devices']
18
19
if user_status['status']['registered'] \
20
and (required_devices & registered_devices)
21
Authy::OneTouch.send_approval_request(
22
id: user.authy_id,
23
message: 'Request to Login to Twilio demo app',
24
details: { 'Email Address' => user.email }
25
)
26
27
status = :onetouch
28
else
29
Authy::API.request_sms(id: user.authy_id)
30
status = :sms
31
end
32
user.update!(authy_status: status)
33
34
pre_init_session!(user.id)
35
36
status.to_s
37
else
38
'unauthorized'
39
end
40
end
41
42
app.get '/logout' do
43
destroy_session!
44
redirect '/login'
45
end
46
end
47
end
48
end
49

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 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.

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

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

1
require 'haml'
2
3
module Routes
4
module Sessions
5
def self.registered(app)
6
app.get '/login' do
7
haml(:login)
8
end
9
10
app.post '/login' do
11
user = User.first(email: params[:email])
12
if user && valid_password?(params[:password], user.password_hash)
13
Authy.api_key = ENV['AUTHY_API_KEY']
14
user_status = Authy::API.user_status(id: user.authy_id)
15
puts user_status
16
required_devices = ['iphone', 'android']
17
registered_devices = user_status['status']['devices']
18
19
if user_status['status']['registered'] \
20
and (required_devices & registered_devices)
21
Authy::OneTouch.send_approval_request(
22
id: user.authy_id,
23
message: 'Request to Login to Twilio demo app',
24
details: { 'Email Address' => user.email }
25
)
26
27
status = :onetouch
28
else
29
Authy::API.request_sms(id: user.authy_id)
30
status = :sms
31
end
32
user.update!(authy_status: status)
33
34
pre_init_session!(user.id)
35
36
status.to_s
37
else
38
'unauthorized'
39
end
40
end
41
42
app.get '/logout' do
43
destroy_session!
44
redirect '/login'
45
end
46
end
47
end
48
end
49

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 just update the authy_status on the user.

Configure the OneTouch callback

configure-the-onetouch-callback-1 page anchor

routes/confirmation.rb

1
module Routes
2
module Confirmation
3
def self.registered(app)
4
app.post '/authy/callback' do
5
authenticate_request!(request)
6
7
request.body.rewind
8
params = JSON.parse(request.body.read)
9
authy_id = params['authy_id']
10
authy_status = params['status']
11
12
begin
13
user = User.first(authy_id: authy_id)
14
user.update!(authy_status: authy_status)
15
rescue => e
16
puts e.message
17
end
18
19
'OK'
20
end
21
22
app.post '/authy/status' do
23
user = User.first(id: current_user)
24
user.authy_status.to_s
25
end
26
27
app.post '/confirm-login' do
28
user = User.first(id: current_user)
29
authy_status = user.authy_status
30
31
user.update(authy_status: :unverified)
32
33
if authy_status == :approved
34
init_session!(user.id)
35
redirect '/protected'
36
else
37
destroy_session!
38
redirect '/login'
39
end
40
end
41
42
app.post '/send-token' do
43
user = User.first(id: current_user)
44
Authy::API.request_sms(id: user.authy_id)
45
'Token has been sent'
46
end
47
48
app.post '/verify-token' do
49
user = User.first(id: current_user)
50
token = Authy::API.verify(id: user.authy_id, token: params[:token])
51
if token.ok?
52
init_session!(user.id)
53
redirect '/protected'
54
else
55
# 'Incorrect code, please try again'
56
destroy_session!
57
redirect '/login'
58
end
59
end
60
end
61
end
62
end
63

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

1
$(document).ready(function() {
2
$("#login-form").submit(function(event) {
3
event.preventDefault();
4
5
var data = $(event.currentTarget).serialize();
6
authyVerification(data);
7
});
8
9
var authyVerification = function (data) {
10
$.post("/login", data, function (result) {
11
resultActions[result]();
12
});
13
};
14
15
var resultActions = {
16
onetouch: function() {
17
$("#authy-modal").modal({ backdrop: "static" }, "show");
18
$(".auth-token").hide();
19
$(".auth-onetouch").fadeIn();
20
monitorOneTouchStatus();
21
},
22
23
sms: function () {
24
$("#authy-modal").modal({ backdrop: "static" }, "show");
25
$(".auth-onetouch").hide();
26
$(".auth-token").fadeIn();
27
requestAuthyToken();
28
},
29
30
unauthorized: function () {
31
$("#error-message").text("The email and password you entered don't match.");
32
}
33
};
34
35
var monitorOneTouchStatus = function () {
36
$.post("/authy/status")
37
.done(function (data) {
38
if (data === "approved" || data === "denied") {
39
$("#confirm-login").submit();
40
} else {
41
setTimeout(monitorOneTouchStatus, 2000);
42
}
43
});
44
}
45
46
var requestAuthyToken = function () {
47
$.post("/authy/request-token")
48
.done(function (data) {
49
$("#authy-token-label").text(data);
50
});
51
}
52
53
$("#logout").click(function() {
54
$("#logout-form").submit();
55
});
56
});
57

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

1
module Routes
2
module Confirmation
3
def self.registered(app)
4
app.post '/authy/callback' do
5
authenticate_request!(request)
6
7
request.body.rewind
8
params = JSON.parse(request.body.read)
9
authy_id = params['authy_id']
10
authy_status = params['status']
11
12
begin
13
user = User.first(authy_id: authy_id)
14
user.update!(authy_status: authy_status)
15
rescue => e
16
puts e.message
17
end
18
19
'OK'
20
end
21
22
app.post '/authy/status' do
23
user = User.first(id: current_user)
24
user.authy_status.to_s
25
end
26
27
app.post '/confirm-login' do
28
user = User.first(id: current_user)
29
authy_status = user.authy_status
30
31
user.update(authy_status: :unverified)
32
33
if authy_status == :approved
34
init_session!(user.id)
35
redirect '/protected'
36
else
37
destroy_session!
38
redirect '/login'
39
end
40
end
41
42
app.post '/send-token' do
43
user = User.first(id: current_user)
44
Authy::API.request_sms(id: user.authy_id)
45
'Token has been sent'
46
end
47
48
app.post '/verify-token' do
49
user = User.first(id: current_user)
50
token = Authy::API.verify(id: user.authy_id, token: params[:token])
51
if token.ok?
52
init_session!(user.id)
53
redirect '/protected'
54
else
55
# 'Incorrect code, please try again'
56
destroy_session!
57
redirect '/login'
58
end
59
end
60
end
61
end
62
end
63

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.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.