Securing your Twilio webhooks in Java

August 20, 2020
Written by
Reviewed by

Header image: Securing your Twilio webhooks in Java

Using webhooks is a common and powerful way to configure your Twilio phone numbers to respond to incoming phone calls or SMS (or faxes). You provide a URL and Twilio makes an HTTP request to that URL when a call or message comes in to find out how to respond.

Diagram of the flow: Someone sends SMS, Twilio makes an HTTP request to your app, the response is passed back to Twilio which sends an SMS reply.

One implication of this is that the URL you provide has to be accessible from the internet, so when you're building your web app to handle the webhooks, how can you know for sure that the requests are really coming from Twilio and not malicious or opportunistic internet scoundrels?

The answer is that all valid webhook requests from Twilio are signed in a header called X-Twilio-Signature. The algorithm used to create the signature is described in detail in our docs.  Briefly, the ingredients of the signature are:

You can use the details of any incoming request to recreate the signature in your application and check that it matches the value in the header. Because your auth token is only known to you and Twilio, if the signatures match you know the request came from Twilio and you can process the message with confidence. The Twilio Java helper library has a RequestValidator class to do just this.

In this post I'll show how to add this validation as a Spring HandlerInterceptor. You can annotate your webhook methods with a custom annotation like @ValidateTwilioSignature to apply the validation and keep validation code out of your @RestController classes.

The code for this post is on GitHub, so start off by cloning the repository and importing it into your IDE. It uses Java 11.

What's in the code?

First of all, have a look in the WebhookHandler class. This class has two methods, one each for handing GET and POST requests for an incoming SMS webhook. They are largely similar, so let's just look at the POST method:

@PostMapping(value = "/webhook", produces = "application/xml")
@ValidateTwilioSignature
@ResponseBody
public String postWebhook(@RequestParam("Body") String messageBody) {

   System.out.println("Valid webhook call, the message Body is: " + messageBody);

   return new MessagingResponse.Builder().message(
       new Message.Builder(
           "Congrats, you're verified by POST \uD83E\uDD96"
       ).build()
   ).build().toXml();
}

[this code on GitHub]

If you've built Spring apps to handle webhook requests before this will look familiar. The response from this method is TwiML to send an SMS response with a cheery dinosaur emoji:

Screenshot of SMS app on a cellphone: "Hello!", "Congrats, you're verified by POST"

The only unusual thing (apart from the dinosaur) is the @ValidateTwilioSignature annotation on the method. Let's see where that comes from.

Creating a custom annotation

The annotation is defined in just a few lines of code in ValidateTwilioSignature.java:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateTwilioSignature {}

[this code on GitHub]

The annotation doesn't need to contain any logic. It just needs to exist so that it can be placed on a method and discovered at runtime.
Intercepting incoming HTTP requestsSpring provides a HandlerInterceptor interface to preprocess incoming HTTP requests. Classes implementing this interface can implement the preHandle method. This method will be called for every incoming HTTP request, and should return a boolean which indicates whether the request should be passed on to the handler method. Handlers have access to the incoming HTTP request, the response, and the target handler method. You can see the whole TwilioValidationHandlerInterceptor on GitHub - let's walk through it in detail:

@Component
public class TwilioValidationHandlerInterceptor implements HandlerInterceptor {

  private final String webhookUrlOverride;
  private final RequestValidator twilioValidator;

  @Autowired
  public TwilioValidationHandlerInterceptor(
    @Value("${twilio.auth.token}") String authToken,
    @Value("${twilio.webhook.url.override}") String webhookUrlOverride) {

      this.webhookUrlOverride = webhookUrlOverride;
      twilioValidator = new RequestValidator(authToken);
  }

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object requestHandler) throws Exception {

    // validation code in here

  }
}

This class is instantiated with two values from application.properties, which you can find in src/main/resources:

  • ${twilio.auth.token} - this is the auth token that you can find at https://twilio.com/console. This is needed to calculate the signature.
  • ${twilio.webhook.url.override} - Twilio calculates its signature based on the webhook URL that you have configured. If your application is behind a proxy that rewrites URLs, such as a load-balancer, SSL-offload or ngrok, then the incoming request that your server sees might have a different URL than the request that Twilio made. In that case, you need to specify this value to be the URL that you configured Twilio to use. Don't include any query parameters on this URL as they will be passed through the proxy unchanged. If requests from Twilio reach your server without being rewritten you can leave this blank.

The constructor uses the authToken to create a RequestValidator which is used in the preHandle method. Lets see that in detail:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object requestHandler) throws Exception {

   if (((HandlerMethod) requestHandler).getMethodAnnotation(ValidateTwilioSignature.class) == null) {
       return true;
   }

First thing, check whether the handler method has our annotation on it. If getMethodAnnotation(ValidateTwilioSignature.class) returns null then the request is destined to reach a method without our custom annotation, so we have no business blocking this request. In this case, return true to exit early and let the application handle the request.

var signatureHeader = request.getHeader("X-Twilio-Signature");
var validationUrl = normalizedRequestUrl(request);

We need to get the X-Twilio-Signature from the headers. The normalizedRequestUrl method builds validationUrl either from the request or the override if you configured that. I won't go into details but you can see the method in full on GitHub.

switch (request.getMethod().toUpperCase()) {
   case "GET":
   case "POST":

       var validationParameters = extractOnlyBodyParams(request, validationUrl);

       if (twilioValidator.validate(validationUrl, validationParameters, signatureHeader)) {
           return true;
       } else {
           logger.warn("Validation failed for {} request to {}", request.getMethod(), validationUrl);
           return validationFailedResponse(response);
       }

   default:
       // only GET and POST are valid
       return validationFailedResponse(response);
}

Now we're ready to validate the request! We need to pass the validationUrl, any body parameters and the signature from the X-Twilio-Signature header.

Extracting parameters from the body of an HTTP request isn't as simple as it sounds using the Servlet API, because HttpServletRequest merges body parameters and query string parameters into a single map. The extractOnlyBodyParams works around this - the implementation is on GitHub.

Once we have extracted the correct values from the request, the validation is the same for GET and POST requests. Any other method will fall into the default branch of the switch and be rejected.

Joining it all together

The last thing to do with the WebhookHandler class, the ValidateTwilioSignature annotation and the TwilioValidationHandlerInterceptor is to configure them all to work together in an application. This is done with a class annotated with Spring's @Configuration annotation. I called mine WebConfig:

@Configuration
public class WebConfig implements WebMvcConfigurer {

   @Autowired
   private TwilioValidationHandlerInterceptor handlerInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(handlerInterceptor);
   }
}

[this code on GitHub]

Running the project

Once you have added your auth token to application.properties (you can find your auth token on your Twilio console), and configured the webhook url override if you need one, you can start up the application with ./mvnw spring-boot:run, set up any proxy you need, and set your webhook URLs for your phone number. Then you're good to go, and safe to boot!

animated gif of a worker putting on safety gear. Caption is "be safe, then safer"

Summing up

If you're using public URLs as webhooks for your Twilio phone numbers, you should definitely validate that HTTP requests are coming from Twilio and not malicious or opportunistic third-parties. This post has shown how to do that for a Spring application, and the same concepts will apply for any other web framework. For more details and recommendations to improve the security of your webhooks, check the Webhook Security page in our docs.

What are you securing? Tell me about it on Twitter or by email - I can't wait to see what you build!

🐦 @MaximumGilliard

📧 mgilliard@twilio.com