Use Twilio's Authy with Google Apps Script web app

Make use of Twilio's TOTP and push authentications using Authy app and Apps Script for your web app.

Use Twilio's Authy with Google Apps Script web app
Use Twilio's Authy with Google Apps Script web app

in case you're wondering what exactly is authy - think of it as a means to enforce 2-factor authentication along with "passwordless" login.

context

i recently started contributing on stack overflow & realised that there has been a rise in the adaptation of web apps (provided by google apps script) and while for the most part it was so far being used as a 'backend' engine, users now feel increasing comfortable exposing the same as their frontend "dashboard" as well.

this leads to administer a certain level of security towards the kind of users you intend to expose your dashboard to and while one can easily modulate that using the deployment module, there are cases where you require your web app to be publicly accessible so other applications could talk to your script (ex: as a webhook) and yet, have a need to restrict everyone (even anonymous) to be able to access your frontend.

icymi: should you not want a complicated 3rd party application for the aforementioned scenario, i've also written about password protected web app that utilises sheets to store & authenticate passwords.

preview

Authy push notifications on Google Apps Script
Authy push notifications on Google Apps Script

demo

you can view the final output of this setup here.

prerequisite

  • a verified twilio account - with their free tier, you get less than 100 auths/month with unlimited users
  • you'd need to have the authy mobile app installed on your phone as well. it is optional at this stage to sign up for their services (which, for a user is completely free); however, you'd be required to provide your email & phone number at the time of testing/implementing this setup.

architecture

twilio's console and the authy api documentation is so darn amazing that you'd not have any glitch setting up your first authy application.

i realise that its a cliché but getting this setup to work for you is as simple as following a 1-2-3 step procedure that are as follows -

  1. create and configure an authy application (make a note of the authy api key, at this stage)
  2. build the script as prescribed within the codebase below (we do not need any sheet associated with this setup, just an apps script; in case you don't know how to initiate a standalone script, you can check that out here)
  3. setup incoming webhook as 'post' request towards the push authentication callbacks

auth types

you can achieve 3 kinds of authentication with authy -

  1. first is the standard sms or a voice call based otp (not covered in this article, just because)
  2. the one that i found interesting was the transactional totp based authentication, which can be used in cases where users are unable to receive sms or calls and have absolutely no internet connectivity
  3. finally, the push-notification based authentication (in some sense, this has become pretty standard too)

this article would cover the last 2 types.

codebase

you can access the entire script on my github repository here; however, the breakup of the code is described below -

adding a user

var authyKey = 'YourAuthyAPIKeyGoesHere';

var userSession = Session.getTemporaryActiveUserKey();
var userProperties = PropertiesService.getUserProperties();

function addNewUser(formData) {
  var addUserURL = "https://api.authy.com/protected/json/users/new";
  var userPayload = {
    "send_install_link_via_sms": false,
    "user[email]" : formData.email,
    "user[cellphone]" : formData.phoneNumber,
    "user[country_code]" : formData.country
  };
  var addUserOptions = {
    "method" : "POST",
    "payload" : userPayload,
    "muteHttpExceptions": true
  };
  addUserOptions.headers = { 
    "X-Authy-API-Key" : authyKey
  };
  var newUser = UrlFetchApp.fetch(addUserURL, addUserOptions);
  var newUserResponse = newUser.getContentText();
  var userResponse = JSON.parse(newUserResponse);
  if (newUser.getResponseCode() == 200) {
    if (userResponse["success"] == true) {
      var authyID = JSON.stringify(userResponse["user"]["id"]);
      userProperties.setProperty(userSession, authyID);
      var newProperties = {};
      newProperties[authyID] = JSON.stringify({
        userLoggedIn: '',
        pushAuthUuid: ''
      });
      userProperties.setProperties(newProperties);
      return 'Registered Successfully!';
    } else {
      return 'Something went wrong :(';
    }
  } else {
    return 'Something went wrong :(';
  }
}

TOTP verification function

function verifyTOTP(token) {
  var authyID = userProperties.getProperty(userSession);
  if (authyID !== null) {
    var verifyTOTPURL = "https://api.authy.com/protected/json/verify/" + token + "/" + authyID;
    var varifyTOTPOptions = {
      "method" : "GET",
      "muteHttpExceptions": true
    };
    varifyTOTPOptions.headers = { 
      "X-Authy-API-Key" : authyKey
    };
    var verifyTOTP = UrlFetchApp.fetch(verifyTOTPURL, varifyTOTPOptions);
    var verifyTOTPResponse = verifyTOTP.getContentText();
    var TOTPResponse = JSON.parse(verifyTOTPResponse);  
    if (verifyTOTP.getResponseCode() == 200) {
      if (TOTPResponse["success"] == "true" && TOTPResponse["token"] == "is valid" && TOTPResponse["message"] == "Token is valid.") {
        var updateProperties = JSON.stringify({
          userLoggedIn: true,
          pushAuthUuid: ''
        });
        userProperties.setProperty(authyID, updateProperties);
        return 'Logging you in!';
      } else {
        return 'Something went wrong :(';
      }
    } else {
      return 'Something went wrong :(';
    }
  } else {
    return 'Please sign-up first to login.'
  }
}

push auth

this is broken into 2 segments -
one is to trigger the notification and another one is a doPost() webhook that retrieves the status from twilio.

push auth trigger

function pushAuth() {
  var authyID = userProperties.getProperty(userSession);
  if (authyID !== null) {
    var pushAuthURL = "https://api.authy.com/onetouch/json/users/" + authyID + "/approval_requests";
    var pushAuthPayload = {
      "message": "Login requested from Google Apps Script."
    };
    var pushAuthOptions = {
      "method" : "POST",
      "payload" : pushAuthPayload,
      "muteHttpExceptions": true
    };
    pushAuthOptions.headers = { 
      "X-Authy-API-Key" : authyKey
    };
    var newPushAuthReq = UrlFetchApp.fetch(pushAuthURL, pushAuthOptions);
    var newPushAuthResponse = newPushAuthReq.getContentText();
    var pushAuthResponse = JSON.parse(newPushAuthResponse);
    if (newPushAuthReq.getResponseCode() == 200) {
      if (pushAuthResponse["success"] == true) {
        var pushAuthUuid = pushAuthResponse["approval_request"]["uuid"];
        var updateProperties = JSON.stringify({
          userLoggedIn: '',
          pushAuthUuid: pushAuthUuid
        });
        userProperties.setProperty(authyID, updateProperties);
        return 'Please check your phone...';
      } else {
        return 'Something went wrong :(';
      }
    } else {
      return 'Something went wrong :(';
    }
  } else {
    return 'Please sign-up first to login.'
  }
}

corresponding webhook

function doPost(e) {
  if (JSON.parse(e.postData.contents).callback_action == "approval_request_status") {
    var pushAuthParams = JSON.parse(e.postData.contents);
    var pushAuthCallbackUuid = pushAuthParams.uuid;
    var authyID = pushAuthParams.authy_id;  
    if (JSON.parse(userProperties.getProperty(authyID)).pushAuthUuid == pushAuthCallbackUuid) {
      if (pushAuthParams.status == "approved") {
        var updateProperties = JSON.stringify({
          userLoggedIn: true,
          pushAuthUuid: pushAuthCallbackUuid
        });
        userProperties.setProperty(authyID, updateProperties);
      }
    }
  }
}

additional notes

  • you can also customise your authy app's images (logo & favicon) and color (background, timer, token etc.) as well
  • tweak the default sms and call authentication methods such that you don't run out of those free twilio credits 😛