Handle SSL Termination when Validating Twilio Webhook Requests in Node.js

September 04, 2024
Written by
Reviewed by
Paul Kamp
Twilion

Handle SSL Termination when Validating Twilio Webhook Requests in Node.js

Ahoy, builders! When working on your Node.js application and integrating it with Twilio, an important security step is Validating Incoming Webhook Requests. If you’re new to this concept, we recommend checking out our documentation on “What is a Webhook”.

Validation is done to ensure all requests are genuinely from Twilio. Your web application should verify that Twilio is the service that sent a webhook before responding to that request. This is important for securing sensitive data and protecting your application – as well as any servers involved – from abuse.

Twilio will sign all inbound requests to your application with an X-Twilio-Signature HTTP header. This signature is created using the parameters sent in the webhook (GET or POST), the URL Twilio used, and your Twilio account’s Auth Token, which serves as the secret key

Securing your Express app with Twilio Node SDK's is straightforward. The Twilio SDK comes with an Express middleware which is ready to use. The twilio.webhook() middleware for Express allows us to verify this signature automatically. In order to use it, you need to pass your Twilio Auth Token as an environment variable and configure the middleware in your Express app. This middleware verifies that the signature in the X-Twilio-Signature header matches what Twilio would expect, ensuring the request is from Twilio.

For a comprehensive tutorial on validating Twilio webhook requests in various languages, refer to this guide: Validating Incoming Twilio Requests.

In this article I will explain what SSL termination is, why it causes issues with Twilio's request validation, and how to handle these issues effectively.

What is SSL Termination?

SSL termination is the process of decrypting SSL (Secure Sockets Layer) or TLS (Transport Layer Security) encrypted traffic at a designated endpoint, such as a load balancer or a proxy server, before forwarding it to the backend servers. This process is essential for handling secure HTTPS connections efficiently, and balancing traffic among multiple servers.

How SSL Termination works

1. Client Request

A client (e.g., a web browser) sends an HTTPS request to your server. This request is encrypted using SSL/TLS, ensuring that data cannot be intercepted or tampered with during transmission.

2. SSL Termination Point

The encrypted request arrives at the SSL termination point, usually a load balancer, reverse proxy, or a dedicated SSL termination device. This device is configured with an SSL certificate and private key, allowing it to decrypt the incoming traffic.

3. Decryption

The SSL termination device decrypts the request, converting it from HTTPS to HTTP. The sensitive data is now in a readable format.

4. Forwarding to Backend Servers

The decrypted HTTP request is forwarded to the appropriate backend server (for example, Twilio) for processing.

5. Server Response

Since the traffic is now unencrypted, the server (or Twilio) will then send back the response that SSL validation has failed due to a mismatch in encryption.

If you are making a request to Twilio, you may also see the Error 11237 shown within the Twilio console. This is because Twilio was expecting a HTTPS request, however received HTTP.

As an example, the URL that Twilio expects (e.g., https://example.com/message) is different from the URL your application sees (e.g., http://example.com/message). This mismatch causes Twilio's request validation to fail, even though the request is legitimate.

While SSL termination offers security, performance, and management benefits, it may introduce a segment of unencrypted traffic within the internal network between the termination point and the backend servers. For highly sensitive data, it’s important to consider additional measures such as SSL re-encryption for secure internal networks. It is also important to double check that any backend services you use have been configured with SSL termination.

Prerequisites

Before we begin looking at SSL Termination with Twilio and the Node.js helper library, you’ll need a few things.

While testing your Node.js application, please ensure the following:

  1. Node.js Installation: Verify that Node.js is installed on your system. You can check this by running node -v in your terminal. If it's not installed, please download and install it from the official Node.js website.
  2. Twilio Account: Ensure you have an active Twilio account with a phone number that can handle messaging and voice calls. You'll also need your Twilio Auth Token, which can be found in your Twilio console under Account Settings.
  3. Ngrok Installation: Install Ngrok, which will be used to expose your local server to the internet. You can download it from the Ngrok website. Follow the installation instructions for your operating system.
  4. Required Node.js Packages: The necessary Node.js packages (express, twilio, and body-parser) should be installed. If not, they will be installed in the steps provided below.

Validate a Twilio Webhook Request without managing SSL termination

The Twilio Node SDK includes the webhook() method which we can use as an Express middleware to validate incoming requests. When applied to an Express route, if the request is unauthorized, the middleware will return a 403 HTTP response.

Here’s an example of a simple Node.js application from our documentation, that processes incoming Twilio webhook requests, using Twilio webhook middleware for Express to validate requests without accounting for SSL termination. I’ll show you how to modify this code to handle SSL termination in the steps that follow.

// You can find your Twilio Auth Token here: https://www.twilio.com/console
// Set at runtime as follows:
// $ TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXX node index.js
//
// This will not work unless you set the TWILIO_AUTH_TOKEN environment
// variable.
const twilio = require('twilio');
const app = require('express')();
const bodyParser = require('body-parser');
const VoiceResponse = require('twilio').twiml.VoiceResponse;
const MessagingResponse = require('twilio').twiml.MessagingResponse;
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/voice', twilio.webhook(), (req, res) => {
  // Twilio Voice URL - receives incoming calls from Twilio
  const response = new VoiceResponse();
  response.say(
    `Thanks for calling!
     Your phone number is ${req.body.From}. I got your call because of Twilio´s
     webhook. Goodbye!`
  );
  res.set('Content-Type', 'text/xml');
  res.send(response.toString());
});
app.post('/message', twilio.webhook(), (req, res) => {
  // Twilio Messaging URL - receives incoming messages from Twilio
  const response = new MessagingResponse();
  response.message(`Your text to me was ${req.body.Body.length} characters long.
                    Webhooks are neat :)`);
  res.set('Content-Type', 'text/xml');
  res.send(response.toString());
});
app.listen(3000);

In this code snippet, if SSL termination occurs upstream, it may cause a URL mismatch that can prevent Twilio from validating the webhook request. When your Twilio webhook URL starts with https:// instead of http://, your request validator might fail locally with Ngrok (or in production) if SSL connections are terminated upstream from your app. This issue arises because the request URL your Express application sees does not match the URL Twilio used to reach your application.

To resolve this during local development with Ngrok, use ngrok http 3000 to expose your local server over HTTP. Additionally, you can handle SSL termination by using the x-forwarded-proto header to reconstruct the original URL that Twilio used. This ensures that the request URL aligns with what Twilio expects, and prevents validation issues.

Understand the x-forwarded-proto header

The X-Forwarded-Proto header is an HTTP header used to indicate the protocol (HTTP or HTTPS) that was used by the client when connecting to a proxy or load balancer, before the request was forwarded to a backend server. This header is typically added by reverse proxies, load balancers, or CDN services that perform SSL termination.

The x-forwarded-proto header is crucial for detecting the original protocol used by a client before SSL termination. When a request is terminated at a load balancer or proxy, your application might only see HTTP traffic, even if the original request was made over HTTPS. This header helps by indicating whether the original request was HTTPS.

In our example, we use x-forwarded-proto to correctly reconstruct the URL Twilio used, ensuring that the webhook validation process can accurately verify the request.

Handle SSL-Termination and reconstructing the original URL

To handle SSL termination, you will need to reconstruct the original URL that Twilio used to send the request. This involves checking the headers that indicate whether the original request was made over HTTPS.

Here’s how you can modify the above code to handle SSL termination:

// You can find your Twilio Auth Token here: https://www.twilio.com/console
// Set at runtime as follows:
// $ TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXX node index.js
//
// This will not work unless you set the TWILIO_AUTH_TOKEN environment
// variable.
const twilio = require("twilio");
const app = require("express")();
const bodyParser = require("body-parser");
const VoiceResponse = require("twilio").twiml.VoiceResponse;
const MessagingResponse = require("twilio").twiml.MessagingResponse;
app.use(bodyParser.urlencoded({ extended: false }));
// Middleware to handle SSL termination and reconstruct the original URL
app.use((req, res, next) => {
 // Determine the protocol, checking if the request came through a proxy
 const protocol = req.headers["x-forwarded-proto"] || req.protocol;
 // Log the original URL received by the application
 console.log(
   "Original URL received:",
   req.protocol + "://" + req.get("host") + req.originalUrl
 );
 // Reconstruct the original URL based on the protocol from headers
 const reconstructedUrl = protocol + "://" + req.get("host") + req.originalUrl;
 // Log the reconstructed URL
 console.log("Reconstructed URL:", reconstructedUrl);
 next();
});
app.post("/voice", twilio.webhook({ protocol: "https" }), (req, res) => {
 // Twilio Voice URL - receives incoming calls from Twilio
 const response = new VoiceResponse();
 response.say(
   `Thanks for calling!
    Your phone number is ${req.body.From}. I got your call because of Twilio's
    webhook. Goodbye!`
 );
 res.set("Content-Type", "text/xml");
 res.send(response.toString());
});
app.post("/message", twilio.webhook({ protocol: "https" }), (req, res) => {
 // Twilio Messaging URL - receives incoming messages from Twilio
 const response = new MessagingResponse();
 response.message(`Your text to me was ${req.body.Body.length} characters long.
                   Webhooks are neat :)`);
 res.set("Content-Type", "text/xml");
 res.send(response.toString());
});
app.listen(3000, () => {
 console.log("Server listening on port 3000");
});

In the above code, a middleware function has been introduced to handle URL reconstruction. This function checks the x-forwarded-proto header, which is typically set by the load balancer or reverse proxy. If this header indicates that the original request was made over HTTPS, the middleware updates the protocol used in the reconstructed URL accordingly, without altering req.protocol directly.

The middleware then reconstructs the URL to reflect the original URL Twilio used to send the request. This ensures that during Twilio's webhook validation, the signature is compared against the correct URL, accounting for any SSL termination or proxy layers.

Additionally, the code includes console.log statements to print both the original URL received by your application and the reconstructed URL. This will help you debug and understand the changes made by the middleware.

Test Your Node.js application

1. Set Up Your Environment

Ensure Node.js is installed on your system. Save the modified code to a file, such as index.js. Then, install the required dependencies by running:

npm install express twilio body-parser

2. Run the Application with Environment Variables

Start the application with your Twilio Auth Token (which you can find in the Twilio Console) set as an environment variable:

TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXX node index.js
// Line to start the node app

3. Test the Endpoints with Ngrok

To expose your local server to the internet and test it with Twilio, you can use Ngrok. First, install Ngrok if you haven't already, then open a new terminal window and run:

ngrok http 3000

Ngrok will provide you with a public URL. You need to configure this URL in your Twilio console for your phone number's incoming message and voice call webhook settings.

  • For Voice: Navigate to the Active Numbers page, pick the phone number to change behavior (or pick the Buy a Number button), find the Voice Configuration section in the phone number's settings, and set the A call comes in Webhook URL to: https://your-ngrok-url.ngrok.io/voice.
  • For Messaging: Similarly, set the Webhook URL in A message comes in for the Messaging Configuration to https://your-ngrok-url.ngrok.io/message.

4. Send a Message or Make a Voice Call

Send a message to your Twilio number to trigger the /message endpoint. Once the message is sent, check your terminal to see the logged original and reconstructed URLs.

For the /voice endpoint, make a voice call to your Twilio number. This will trigger the voice handling code, allowing you to see how the application processes voice requests.

5. Check the Logs

When your application runs successfully, you will see console logs printed that show the difference between the original and reconstructed URLs:

Server listening on port 3000 Original 
URL received: http://your-ngrok-url.ngrok.io/message 
Reconstructed URL: https://your-ngrok-url.ngrok.io/message

This output confirms that the application correctly reconstructs the URL to match what Twilio originally used.

Conclusion

Handling SSL termination is crucial when validating Twilio webhook requests in environments where SSL termination occurs upstream from your application. By reconstructing the original URL that Twilio used, you can ensure that your application correctly validates these requests, maintaining the security of your integration. Implementing this solution will prevent the validation issues that arise due to URL mismatches, allowing your Twilio-powered application to function smoothly in SSL-terminated environments.

Additional resources

The resources below may be helpful for handling incoming webhook requests:

Additional Resources:

As always, you can reach our Support Team at Twilio’s Help Center .

We can’t wait to see what you build!


Partha Paul is a Senior Technical Support Engineer with the Platform and Applications Support Team at Twilio. With a keen passion for addressing customer challenges, he leverages his expertise in Twilio APIs, helper libraries, and Node.js to deliver effective solutions. He can be reached at papaul [at] twilio.com or LinkedIn .