In this guide we'll cover how to secure your Servlet application by validating incoming requests to your Twilio webhooks that are, in fact, from Twilio.
With a few lines of code, we'll write a custom filter for our Servlet app that uses the Twilio Java SDK's validator utility. This filter will then be invoked on the relevant paths that accept Twilio webhooks to confirm that incoming requests genuinely originated from Twilio.
Let's get started!
The Twilio Java SDK includes a RequestValidator
class we can use to validate incoming requests.
We could include our request validation code as part of our Servlet, but this is a perfect opportunity to write a Java filter. This way we can reuse our validation logic across all our Servlets which accept incoming requests from Twilio.
Confirm incoming requests to your Servlets are genuine with this filter.
_84package guide;_84_84import com.twilio.security.RequestValidator;_84_84import javax.servlet.*;_84import javax.servlet.http.HttpServletRequest;_84import javax.servlet.http.HttpServletResponse;_84import java.io.IOException;_84import java.util.Arrays;_84import java.util.Collections;_84import java.util.List;_84import java.util.Map;_84import java.util.stream.Collectors;_84_84public class TwilioRequestValidatorFilter implements Filter {_84_84 private RequestValidator requestValidator;_84_84 @Override_84 public void init(FilterConfig filterConfig) throws ServletException {_84 requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));_84 }_84_84 @Override_84 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)_84 throws IOException, ServletException {_84_84 boolean isValidRequest = false;_84 if (request instanceof HttpServletRequest) {_84 HttpServletRequest httpRequest = (HttpServletRequest) request;_84_84 // Concatenates the request URL with the query string_84 String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);_84 // Extracts only the POST parameters and converts the parameters Map type_84 Map<String, String> postParams = extractPostParams(httpRequest);_84 String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");_84_84 isValidRequest = requestValidator.validate(_84 pathAndQueryUrl,_84 postParams,_84 signatureHeader);_84 }_84_84 if(isValidRequest) {_84 chain.doFilter(request, response);_84 } else {_84 ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);_84 }_84 }_84_84 @Override_84 public void destroy() {_84 // Nothing to do_84 }_84_84 private Map<String, String> extractPostParams(HttpServletRequest request) {_84 String queryString = request.getQueryString();_84 Map<String, String[]> requestParams = request.getParameterMap();_84 List<String> queryStringKeys = getQueryStringKeys(queryString);_84_84 return requestParams.entrySet().stream()_84 .filter(e -> !queryStringKeys.contains(e.getKey()))_84 .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));_84 }_84_84 private List<String> getQueryStringKeys(String queryString) {_84 if(queryString == null || queryString.length() == 0) {_84 return Collections.emptyList();_84 } else {_84 return Arrays.stream(queryString.split("&"))_84 .map(pair -> pair.split("=")[0])_84 .collect(Collectors.toList());_84 }_84 }_84_84 private String getRequestUrlAndQueryString(HttpServletRequest request) {_84 String queryString = request.getQueryString();_84 String requestUrl = request.getRequestURL().toString();_84 if(queryString != null && !queryString.equals("")) {_84 return requestUrl + "?" + queryString;_84 }_84 return requestUrl;_84 }_84}
The doFilter
method will be executed before our Servlet, so it's here where we will validate that the request originated genuinely from Twilio, and prevent it from reaching our Servlet if it didn't.
First, we gather the relevant request metadata (URL, query string and X-TWILIO-SIGNATURE
header) and the POST
parameters. We then pass this data onto the validate
method of RequestValidator
, which will return whether the validation was successful or not.
If the validation turns out successful, we continue executing other filters and eventually our Servlet. If it is unsuccessful, we stop the request and send a 403 - Forbidden
response to the requester, in this case, Twilio.
Now we're ready to apply our filter to any path in our Servlet application that handles incoming requests from Twilio.
Apply a custom Twilio request validation filter to a set of Servlets used for Twilio webhooks.
_36<?xml version="1.0" encoding="UTF-8"?>_36<web-app version="3.0"_36 metadata-complete="true"_36 xmlns="http://java.sun.com/xml/ns/javaee"_36 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"_36 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">_36_36 <servlet>_36 <servlet-name>voiceHandler</servlet-name>_36 <servlet-class>guide.VoiceHandlerServlet</servlet-class>_36 </servlet>_36_36 <servlet>_36 <servlet-name>messageHandler</servlet-name>_36 <servlet-class>guide.MessageHandlerServlet</servlet-class>_36 </servlet>_36_36 <servlet-mapping>_36 <servlet-name>voiceHandler</servlet-name>_36 <url-pattern>/voice</url-pattern>_36 </servlet-mapping>_36_36 <servlet-mapping>_36 <servlet-name>messageHandler</servlet-name>_36 <url-pattern>/message</url-pattern>_36 </servlet-mapping>_36_36 <filter>_36 <filter-name>requestValidatorFilter</filter-name>_36 <filter-class>guide.TwilioRequestValidatorFilter</filter-class>_36 </filter>_36 <filter-mapping>_36 <filter-name>requestValidatorFilter</filter-name>_36 <url-pattern>/*</url-pattern>_36 </filter-mapping>_36</web-app>
To use the filter just add <filter>
and <filter-mapping>
sections to your web.xml
. No changes are needed in the actual Servlets.
In the <filter>
section we give a name to be used within your web.xml
. In this case requestValidatorFilter
. We also point to the filter class using its fully qualified name.
In the <filter-mapping>
section, we configure what paths in our container will use TwilioRequestFilter
when receiving a request. It uses URL patterns to select those paths, and you can have multiple <url-pattern>
elements in this section. Since we want to apply the filter to both Servlets, we use their common root path.
Note: If your Twilio webhook URLs start with https://
instead of http://
, your request validator may fail locally when you use Ngrok or in production if your stack terminates SSL connections upstream from your app. This is because the request URL that your Servlet application sees does not match the URL Twilio used to reach your application.
To fix this for local development with Ngrok, use http://
for your webhook instead of https://
. To fix this in your production app, your filter will need to reconstruct the request's original URL using request headers like X-Original-Host
and X-Forwarded-Proto
, if available.
If you write tests for your Servlets those tests may fail where you use your Twilio request validation filter. Any requests your test suite sends to those Servlets will fail the filter's validation check.
To fix this problem we recommend adding an extra check in your filter, like so, telling it to only reject incoming requests if your app is running in production.
Use this version of the custom filter if you test your Servlets.
_90package guide;_90_90import com.twilio.security.RequestValidator;_90_90import javax.servlet.*;_90import javax.servlet.http.HttpServletRequest;_90import javax.servlet.http.HttpServletResponse;_90import java.io.IOException;_90import java.util.Arrays;_90import java.util.Collections;_90import java.util.List;_90import java.util.Map;_90import java.util.stream.Collectors;_90_90public class TwilioRequestValidatorFilter implements Filter {_90_90 private final String currentEnvironment = System.getenv("ENVIRONMENT");_90_90 private RequestValidator requestValidator;_90_90 @Override_90 public void init(FilterConfig filterConfig) throws ServletException {_90 requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));_90 }_90_90 @Override_90 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)_90 throws IOException, ServletException {_90_90 boolean isValidRequest = false;_90 if (request instanceof HttpServletRequest) {_90 HttpServletRequest httpRequest = (HttpServletRequest) request;_90_90 // Concatenates the request URL with the query string_90 String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);_90 // Extracts only the POST parameters and converts the parameters Map type_90 Map<String, String> postParams = extractPostParams(httpRequest);_90 String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");_90_90 isValidRequest = requestValidator.validate(_90 pathAndQueryUrl,_90 postParams,_90 signatureHeader);_90 }_90_90 if(isValidRequest || environmentIsTest()) {_90 chain.doFilter(request, response);_90 } else {_90 ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);_90 }_90 }_90_90 @Override_90 public void destroy() {_90 // Nothing to do_90 }_90_90 private boolean environmentIsTest() {_90 return "test".equals(currentEnvironment);_90 }_90_90 private Map<String, String> extractPostParams(HttpServletRequest request) {_90 String queryString = request.getQueryString();_90 Map<String, String[]> requestParams = request.getParameterMap();_90 List<String> queryStringKeys = getQueryStringKeys(queryString);_90_90 return requestParams.entrySet().stream()_90 .filter(e -> !queryStringKeys.contains(e.getKey()))_90 .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));_90 }_90_90 private List<String> getQueryStringKeys(String queryString) {_90 if(queryString == null || queryString.length() == 0) {_90 return Collections.emptyList();_90 } else {_90 return Arrays.stream(queryString.split("&"))_90 .map(pair -> pair.split("=")[0])_90 .collect(Collectors.toList());_90 }_90 }_90_90 private String getRequestUrlAndQueryString(HttpServletRequest request) {_90 String queryString = request.getQueryString();_90 String requestUrl = request.getRequestURL().toString();_90 if(queryString != null && queryString != "") {_90 return requestUrl + "?" + queryString;_90 }_90 return requestUrl;_90 }_90}
Validating requests to your Twilio webhooks is a great first step for securing your Twilio application. We recommend reading over our full security documentation for more advice on protecting your app, and the Anti-Fraud Developer's Guide in particular.
To learn more about securing your Servlet application in general, check out the security considerations page in the official Oracle docs.