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 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!

Create a custom filter

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.

Loading Code Samples...
Language
SDK Version:
  • 7.x
package guide;

import com.twilio.security.RequestValidator;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TwilioRequestValidatorFilter implements Filter {

    private RequestValidator requestValidator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        boolean isValidRequest = false;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // Concatenates the request URL with the query string
            String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);
            // Extracts only the POST parameters and converts the parameters Map type
            Map<String, String> postParams = extractPostParams(httpRequest);
            String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");

            isValidRequest = requestValidator.validate(
                    pathAndQueryUrl,
                    postParams,
                    signatureHeader);
        }

        if(isValidRequest) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    @Override
    public void destroy() {
        // Nothing to do
    }

    private Map<String, String> extractPostParams(HttpServletRequest request) {
        String queryString = request.getQueryString();
        Map<String, String[]> requestParams = request.getParameterMap();
        List<String> queryStringKeys = getQueryStringKeys(queryString);

        return requestParams.entrySet().stream()
                .filter(e -> !queryStringKeys.contains(e.getKey()))
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));
    }

    private List<String> getQueryStringKeys(String queryString) {
        if(queryString == null || queryString.length() == 0) {
            return Collections.emptyList();
        } else {
            return Arrays.stream(queryString.split("&"))
                    .map(pair -> pair.split("=")[0])
                    .collect(Collectors.toList());
        }
    }

    private String getRequestUrlAndQueryString(HttpServletRequest request) {
        String queryString = request.getQueryString();
        String requestUrl = request.getRequestURL().toString();
        if(queryString != null && queryString != "") {
            return requestUrl + "?" + queryString;
        }
        return requestUrl;
    }
}
Confirm incoming requests to your Servlets are genuine with this filter.
Use Servlet filter to validate Twilio requests

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

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

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

Loading Code Samples...
Language
SDK Version:
  • 7.x
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         metadata-complete="true"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <servlet>
        <servlet-name>voiceHandler</servlet-name>
        <servlet-class>guide.VoiceHandlerServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>messageHandler</servlet-name>
        <servlet-class>guide.MessageHandlerServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>voiceHandler</servlet-name>
        <url-pattern>/voice</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>messageHandler</servlet-name>
        <url-pattern>/message</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>requestValidatorFilter</filter-name>
        <filter-class>guide.TwilioRequestValidatorFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestValidatorFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
Apply a custom Twilio request validation filter to a set of Servlets used for Twilio webhooks.
Apply the request validation filter to a set of Servlets

Apply a custom Twilio request validation filter to a set of Servlets used for Twilio webhooks.

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 webook 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

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.

Loading Code Samples...
Language
SDK Version:
  • 7.x
package guide;

import com.twilio.security.RequestValidator;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TwilioRequestValidatorFilter implements Filter {

    private final String currentEnvironment = System.getenv("ENVIRONMENT");

    private RequestValidator requestValidator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        boolean isValidRequest = false;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // Concatenates the request URL with the query string
            String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);
            // Extracts only the POST parameters and converts the parameters Map type
            Map<String, String> postParams = extractPostParams(httpRequest);
            String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");

            isValidRequest = requestValidator.validate(
                    pathAndQueryUrl,
                    postParams,
                    signatureHeader);
        }

        if(isValidRequest || environmentIsTest()) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    @Override
    public void destroy() {
        // Nothing to do
    }

    private boolean environmentIsTest() {
        return "test".equals(currentEnvironment);
    }

    private Map<String, String> extractPostParams(HttpServletRequest request) {
        String queryString = request.getQueryString();
        Map<String, String[]> requestParams = request.getParameterMap();
        List<String> queryStringKeys = getQueryStringKeys(queryString);

        return requestParams.entrySet().stream()
                .filter(e -> !queryStringKeys.contains(e.getKey()))
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));
    }

    private List<String> getQueryStringKeys(String queryString) {
        if(queryString == null || queryString.length() == 0) {
            return Collections.emptyList();
        } else {
            return Arrays.stream(queryString.split("&"))
                    .map(pair -> pair.split("=")[0])
                    .collect(Collectors.toList());
        }
    }

    private String getRequestUrlAndQueryString(HttpServletRequest request) {
        String queryString = request.getQueryString();
        String requestUrl = request.getRequestURL().toString();
        if(queryString != null && queryString != "") {
            return requestUrl + "?" + queryString;
        }
        return requestUrl;
    }
}
Use this version of the custom filter if you test your Servlets.
An improved request validation filter, useful for testing

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

What’s next?

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.

Hector Ortega
Andrew Baker
Kevin Whinnery
Brian Partridge

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
SDK Version:
  • 7.x
package guide;

import com.twilio.security.RequestValidator;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TwilioRequestValidatorFilter implements Filter {

    private RequestValidator requestValidator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        boolean isValidRequest = false;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // Concatenates the request URL with the query string
            String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);
            // Extracts only the POST parameters and converts the parameters Map type
            Map<String, String> postParams = extractPostParams(httpRequest);
            String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");

            isValidRequest = requestValidator.validate(
                    pathAndQueryUrl,
                    postParams,
                    signatureHeader);
        }

        if(isValidRequest) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    @Override
    public void destroy() {
        // Nothing to do
    }

    private Map<String, String> extractPostParams(HttpServletRequest request) {
        String queryString = request.getQueryString();
        Map<String, String[]> requestParams = request.getParameterMap();
        List<String> queryStringKeys = getQueryStringKeys(queryString);

        return requestParams.entrySet().stream()
                .filter(e -> !queryStringKeys.contains(e.getKey()))
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));
    }

    private List<String> getQueryStringKeys(String queryString) {
        if(queryString == null || queryString.length() == 0) {
            return Collections.emptyList();
        } else {
            return Arrays.stream(queryString.split("&"))
                    .map(pair -> pair.split("=")[0])
                    .collect(Collectors.toList());
        }
    }

    private String getRequestUrlAndQueryString(HttpServletRequest request) {
        String queryString = request.getQueryString();
        String requestUrl = request.getRequestURL().toString();
        if(queryString != null && queryString != "") {
            return requestUrl + "?" + queryString;
        }
        return requestUrl;
    }
}
SDK Version:
  • 7.x
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         metadata-complete="true"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <servlet>
        <servlet-name>voiceHandler</servlet-name>
        <servlet-class>guide.VoiceHandlerServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>messageHandler</servlet-name>
        <servlet-class>guide.MessageHandlerServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>voiceHandler</servlet-name>
        <url-pattern>/voice</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>messageHandler</servlet-name>
        <url-pattern>/message</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>requestValidatorFilter</filter-name>
        <filter-class>guide.TwilioRequestValidatorFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestValidatorFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
SDK Version:
  • 7.x
package guide;

import com.twilio.security.RequestValidator;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TwilioRequestValidatorFilter implements Filter {

    private final String currentEnvironment = System.getenv("ENVIRONMENT");

    private RequestValidator requestValidator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        requestValidator = new RequestValidator(System.getenv("TWILIO_AUTH_TOKEN"));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        boolean isValidRequest = false;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // Concatenates the request URL with the query string
            String pathAndQueryUrl = getRequestUrlAndQueryString(httpRequest);
            // Extracts only the POST parameters and converts the parameters Map type
            Map<String, String> postParams = extractPostParams(httpRequest);
            String signatureHeader = httpRequest.getHeader("X-Twilio-Signature");

            isValidRequest = requestValidator.validate(
                    pathAndQueryUrl,
                    postParams,
                    signatureHeader);
        }

        if(isValidRequest || environmentIsTest()) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    @Override
    public void destroy() {
        // Nothing to do
    }

    private boolean environmentIsTest() {
        return "test".equals(currentEnvironment);
    }

    private Map<String, String> extractPostParams(HttpServletRequest request) {
        String queryString = request.getQueryString();
        Map<String, String[]> requestParams = request.getParameterMap();
        List<String> queryStringKeys = getQueryStringKeys(queryString);

        return requestParams.entrySet().stream()
                .filter(e -> !queryStringKeys.contains(e.getKey()))
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));
    }

    private List<String> getQueryStringKeys(String queryString) {
        if(queryString == null || queryString.length() == 0) {
            return Collections.emptyList();
        } else {
            return Arrays.stream(queryString.split("&"))
                    .map(pair -> pair.split("=")[0])
                    .collect(Collectors.toList());
        }
    }

    private String getRequestUrlAndQueryString(HttpServletRequest request) {
        String queryString = request.getQueryString();
        String requestUrl = request.getRequestURL().toString();
        if(queryString != null && queryString != "") {
            return requestUrl + "?" + queryString;
        }
        return requestUrl;
    }
}