Skip to contentSkip to navigationSkip to topbar
Rate this Page:

Secure your Servlet app by validating incoming Twilio requests


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(link takes you to an external page) 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!


Create a custom filter

create-a-custom-filter page anchor

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(link takes you to an external page). This way we can reuse our validation logic across all our Servlets which accept incoming requests from Twilio.

Use Servlet filter to validate Twilio requests

use-servlet-filter-to-validate-twilio-requests page anchor

Confirm incoming requests to your Servlets are genuine with this filter.


_84
package guide;
_84
_84
import com.twilio.security.RequestValidator;
_84
_84
import javax.servlet.*;
_84
import javax.servlet.http.HttpServletRequest;
_84
import javax.servlet.http.HttpServletResponse;
_84
import java.io.IOException;
_84
import java.util.Arrays;
_84
import java.util.Collections;
_84
import java.util.List;
_84
import java.util.Map;
_84
import java.util.stream.Collectors;
_84
_84
public 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.


Use the filter with our Twilio webhooks

use-the-filter-with-our-twilio-webhooks page anchor

Now we're ready to apply our filter to any path in our Servlet application that handles incoming requests from Twilio.

Apply the request validation filter to a set of Servlets

apply-the-request-validation-filter-to-a-set-of-servlets page anchor

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.


Disable request validation during testing

disable-request-validation-during-testing page anchor

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.

An improved request validation filter, useful for testing

an-improved-request-validation-filter-useful-for-testing page anchor

Use this version of the custom filter if you test your Servlets.


_90
package guide;
_90
_90
import com.twilio.security.RequestValidator;
_90
_90
import javax.servlet.*;
_90
import javax.servlet.http.HttpServletRequest;
_90
import javax.servlet.http.HttpServletResponse;
_90
import java.io.IOException;
_90
import java.util.Arrays;
_90
import java.util.Collections;
_90
import java.util.List;
_90
import java.util.Map;
_90
import java.util.stream.Collectors;
_90
_90
public 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(link takes you to an external page).


Rate this Page: