Using Twilio Functions with the Okta Telephony Inline Hook

Okta’s Identity Engine introduced an Inline Hook for Telephony effectively allowing you to replace Okta’s inbuilt SMS solution with your own provider. In this short post I will detail how to leverage Twilio Functions to deliver OTP codes via SMS or Voice call. This is largely an adaption of the Okta Developer Instructions for leveraging Twilio using Glitch. While Glitch is a great service for learning and prototyping it may not be suitable for a production deployment.
The steps for this are as follows:

  1. Setup Twilio Account
  2. Obtain/Re-use Phone Numbers on Twilio
  3. Create the Twilio Service & Function
  4. Configure the Phone Authenticator
  5. Configure the Inline Hook.
  6. Preview and Test the Inline Hook.
  7. Enable the Inline Hook

Set Up Twilio Account

There are a couple of steps required within your Twilio account to setup this demonstration. Firstly, if you do not yet have a Twilio Account you can head over here and create one. You’ll get a small amount of credit, enough to try this out. I recommend reading through this guide to understand how to work with the trial account.

Obtain Twilio Phone Numbers

Obtaining a phone number with Twilio is straight forward from a technical perspective. They can be purchased in the Twilio console follow the instructions here.

Unfortunately, while the technical process of obtaining a number is straightforward the regulatory and compliance processes can be complex. The level of documentation and processes required vary by country. I recommend you review Twilio’s Guidelines documentation for more information on this.

Note that in this example we setup a single phone number to demonstrate how to do it. In practice we would recommend the use of a Twilio Messaging Service which supports sms from multiple senders, including Alphanumeric and has optimisation logic for deliverability and cost.

Create Twilio Function

Twilio Functions is part of their Serverless offering. For full documentation and additional information look here. To setup a function to receive our inline hook we need to create a service and then add then create the function.
To setup the service & function complete the following steps.

  1. Navigate to Functions and Assets in the developer console and click Create Service.
  1. Name your service noting that the service name becomes part of the URL to access the service in the form of URL: [Service name]-[auto-generated numbers].twil.io. The URL will not be seen by end users just the administrators so this should not be a concern.
  2. Once the service is created click Add + and then Add Function to create a new blank function.
  1. When you do this you’re given a generic function path like /path_1 I recommend changing this to something more informative like telephonyHook. This will make your full function URL something like https://[Service name]-[auto-generated numbers].twil.io/telephonyHook
  2. Copy and paste the code from GitHub or below into the functions window.

exports.handler = async function(context, event, callback) {

  const client = context.getTwilioClient();

  // Prints to console log the name of the user signing in and requesting OTP
  console.log(" ");
  console.log(event)
  console.log(
    "Processing OTP delivery for " +
      event.data.userProfile["firstName"] +
      " " +
      event.data.userProfile["lastName"] +
      " " +
      event.data.userProfile["login"]
  );

  // Saves phone number for the user requesting OTP in the variable userPhoneNumber and prints it to the console log
  var userPhoneNumber = event.data.messageProfile["phoneNumber"];

  // Saves OTP code from Okta to send to user via SMS provider
  var userOtpCode = event.data.messageProfile["otpCode"];

  // Retrieves the sender/from phone number
  var from = process.env.FROM_PHONE_NUMBER;
  const actionKey = "com.okta.telephony.action";
  const actionVal = "PENDING";
  const providerName = "TWILIO_HOOK";

  console.log(`To:${userPhoneNumber}. From:${from}. OTP:${userOtpCode}`)

  // Uses userPhoneNumber and userOTP variables to send to third-party telephony provider
  if (
    event.data.messageProfile["deliveryChannel"].toLowerCase() === "sms"
  ) {
    console.log("Sending SMS ...");
    client.messages
        .create({
          body: `Your IAMSE code is ${userOtpCode}`,
          to: userPhoneNumber,
          from: from,
        })
        .then((message) => {
          console.log("Successfully sent sms: " + message.sid);

          const resp = {
            commands: [
              {
                type: actionKey,
                value: [
                  {
                    status: actionVal,
                    provider: providerName,
                    transactionId: message.sid,
                  },
                ],
              },
            ],
          }
          return callback(null, resp);
        })
        .catch((error) => {
          console.log("Error sending message: " + error);
          const errorResp = {
            error: {
              errorSummary: error.message,
              errorCauses: [
                {
                  errorSummary: error.status,
                  reason: error.moreInfo,
                  location: error.detail,
                },
              ],
            },
          };
          callback(errorResp);
        });
  } else {
    console.log("Making CALL ...");
    // Add space to OTP digits for correct pronunciation
    userOtpCode = userOtpCode.replace(/\B(?=(\d{1})+(?!\d))/g, " ");

    client.calls
      .create({ 
        to: userPhoneNumber,
        from: from, 
        twiml: `<Response><Say>Your IAMSE code is ${userOtpCode}</Say></Response>`
      })
      .then((call) => {
          console.log("Successfully started call: " + call.sid);
          const resp = {
            commands: [
              {
                type: actionKey,
                value: [
                  {
                    status: "SUCCESSFUL",
                    provider: providerName,
                    transactionId: call.sid,
                  },
                ],
              },
            ],
          };
          return callback(null, resp);
      })
      .catch((error) => {
          console.log("Error making call: " + error);
          const errorResp = {
            error: {
              errorSummary: error.message,
              errorCauses: [
                {
                  errorSummary: error.status,
                  reason: error.moreInfo,
                  location: error.detail,
                },
              ],
            },
          };
          callback(errorResp);
      });
  }
};
  1. Next we need to add the phone number as the environment variable FROM_PHONE_NUMBER and set it to the phone number you obtained previously. To do this click Environment Variables under the Settings & More heading on the bottom left. Fill in the data as shown below and click add. Additionally, make sure the check box for Add my Twilio Credentials (ACCOUNT_SID) and (AUTH_TOKEN) to ENV
  1. Before you can use the function you need to ensure that it is Saved and deployed. Click Save at the bottom of the function and then click the Deploy All. After a few minutes you should see it successfully deployed in the log window like as shown below.
  1. While you’re here grab the URL with the Copy URL button and toggle Live Logs to on as shown below.

Configure Phone Authenticator

To configure the phone authenticator follow the Okta Developer Documentation for enabling the Phone Authenticator and updating an authentication policy.

Configure the Inline Hook in Okta

Complete the following steps within the Okta Admin Console to configure the inline hook.

  1. Navigate to Workflow -> Inline Hooks, click Add Inline Hook and select the Telephony type as shown below.
  1. Fill in the details including the URL you saved in the previous step. Select HTTP Headers as the Authentication option and enter authorization as the Authentication field and anything as the Authentication secret. Then click Save.
    Note: The Twilio function is by default made publically available without any security enabled. This makes it easy to use for a proof of concept but it should not be used like this moving forward. It is recommended that you protect your function as outlined here.

Testing the Inline Hook

Next we will test the inline hook we have created. The simplest way to do this is to leverage the preview functionality within the Okta Admin Console as described below.

  1. Select the Preview tab for the inline hook. Then select a test user by starting to type in the data.userProfile box. Note, the test user doesn’t need to have up to date details as we will be modifying the request anyway. Set the requestType to MFA Verification as shown below
  1. Next select Generate Request and click Edit. Now modify the phone number to your test phone number ensuring that it is in E.164 format.
  1. Once you have edited the phone number be sure to select save and then click View Response. This will send a request to the Twilio Function. All going well you’ll get a response like the below indicating a successful queuing of the SMS with Twilio. Note that this response does not indicate successful delivery of the SMS merely that Twilio has accepted the message to send and sent it upstream towards the carriers. You should also receive an SMS like the below from your Twilio phone number.
  1. To test a call simply edit the request and change the delivery channel to CALL. If you have any challenges or the function returns an error the best bet is to head to the Twilio Function Console to debug. A successful call will generate a log like the below. Note that if the live log doesn’t seem to be working simply refresh the page and ensure that Live Logs is toggled on again before re-testing.

Enabling the Inline Hook

Finally confirm that the inline hook is Active in the console as shown. If not you can activate it by selecting the Actions menu and Activate.

Final Thoughts

This blog demonstrates a simple Twilio function for sending Okta OTP codes leveraging the Okta Inline Hook functionality. In a production deployment it is recommended that you leverage a messaging service with multiple numbers and senders as best determined by your target demographics. Twilio is able to work through number selection and regulatory requirements.

10 thoughts on “Using Twilio Functions with the Okta Telephony Inline Hook

  1. Thanks for this great guide. Everything seemed to work out until testing at the end. I followed your guide above but keep getting a 403 error in preview. What might cause this?

    1. There are several places that a 403 could be generated in this process.

      From Okta to Twilio may occur because there are protections enabled on the function.

      Within Twilio itself it may be occurring because you’re using a trial account with sending restrictions or the incorrect permissions are used. You can leverage the Twilio logs to dive deeper into the error.

      1. Just in case anyone else is reading this or comes across this article, Twilio by default sets the functions up as “Protected”, meaning you need to provide authentication to connect. If you are just doing a proof of concept, you can switch that to “Public” which should allow you to connect. If you go into Functions and Assets > Services > Your created service. Then find your function listed, there’s a little icon to the left of the vertical ellipsis. From there, you have options to change it to Private, Protected or Public. Obviously it should not be left on Public for any production use.

        Thank you Toby for the article! It worked great, now just need to dive into how to properly secure it.

  2. Getting an Error when previewing the response in Okta, “Hook “Twilio Okta test” execution failed. Could not deserialize inline hook response due to error at Line 1 Column 13″. Any idea’s what could be causing this?

    Thanks!

    1. Please confirm within the Twilio Logs either in the console live view or the execution logs that there are no errors occurring in the function execution.

  3. Thank you very much for this example. I was able to follow it without too much difficulty, but encountered a problem that turned out to be related to the response that’s sent back to Okta for an SMS send. From what I can tell in the code, it’s hard-coded as PENDING, which causes Okta to send its own SMS as a fall-back method (thank you to Okta support for that nugget). Any particular reason we shouldn’t change that to “SUCCESSFUL” before returning the callback?

    1. Apologies, I thought I had updated this. Yes it needs to be set to SUCCESSFUL anything else will cause fallback to the Okta provider.

  4. Thank you so much for this tutorial Toby. I was able to successfully deploy this in a test environment however, I’d like to ensure we are using Basic Authentication prior to taking this into production. I was trying to follow the tutorial from Twilio to include Basic Authentication but I’m not sure where I’d place their code (In a separate Function or inside the original function that’s currently working). Any thoughts on this? I apologize as I’m not well versed in programing.

Leave a Reply