Secure your C# / ASP.NET WEB API app by validating incoming Twilio requests

This guide is for ASP.NET Web API on the .NET Framework. For ASP.NET Core, see this guide. For ASP.NET MVC on the .NET Framework, see this guide.

In this guide, we’ll cover how to secure your C# / ASP.NET Web API 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 attribute for our ASP.NET app that uses the Twilio C# SDK’s validator utility. This filter will then be invoked on the controller actions that accept Twilio webhooks to confirm that incoming requests genuinely originated from Twilio.

Let’s get started!

Create a custom filter attribute

The Twilio C# SDK includes a RequestValidator class we can use to validate incoming requests.

We could include our request validation code as part of our controller, but this is a perfect opportunity to write an action filter attribute. This way we can reuse our validation logic across all our controller actions which accept incoming requests from Twilio.

Loading Code Samples...
Language
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Twilio.Security;

namespace ValidateRequestExample.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class ValidateTwilioRequestAttribute : ActionFilterAttribute
    {
        private readonly string _authToken;
        private readonly string _urlSchemeAndDomain;

        public ValidateTwilioRequestAttribute()
        {
            _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
            _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];
        }

        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            if (!await IsValidRequestAsync(actionContext.Request))
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.Forbidden,
                    "The Twilio request is invalid"
                );
            }

            await base.OnActionExecutingAsync(actionContext, cancellationToken);
        }

        private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)
        {
            var headerExists = request.Headers.TryGetValues(
                "X-Twilio-Signature", out IEnumerable<string> signature);
            if (!headerExists) return false;

            var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;
            var formData = await GetFormDataAsync(request.Content);
            return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());
        }

        private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)
        {
            string postData;
            using (var stream = new StreamReader(await content.ReadAsStreamAsync()))
            {
                stream.BaseStream.Position = 0;
                postData = await stream.ReadToEndAsync();
            }

            if(!String.IsNullOrEmpty(postData) && postData.Contains("="))
            {
                return postData.Split('&')
                    .Select(x => x.Split('='))
                    .ToDictionary(
                        x => Uri.UnescapeDataString(x[0]),
                        x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))
                    );
            }

            return new Dictionary<string, string>();
        }
    }
}
Confirm incoming requests to your controllers are genuine with this filter.
ASP.NET Web API filter attribute to validate Twilio requests

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

To validate an incoming request genuinely originated from Twilio, we first need to create an instance of the RequestValidator class passing it our Twilio Auth Token. Then we cal its Validate method passing the requester URL, the form params, and the Twilio request signature.

That method will return True if the request is valid or False if it isn’t. Our filter attribute then either continues processing the action or returns a 403 HTTP response for invalid requests.

Use the filter attribute with our Twilio webhooks

Now we’re ready to apply our filter attribute to any controller action in our ASP.NET application that handles incoming requests from Twilio.

Loading Code Samples...
Language
using System.Net.Http;
using System.Text;
using System.Web.Http;
using Twilio.TwiML;
using Twilio.TwiML.Messaging;
using ValidateRequestExample.Filters;

namespace ValidateRequestExample.Controllers
{
    public class TwilioMessagingRequest
    {
        public string Body { get; set; }
    }

    public class TwilioVoiceRequest
    {
        public string From { get; set; }
    }

    public class IncomingController : ApiController
    {
        [Route("voice")]
        [AcceptVerbs("POST")]
        [ValidateTwilioRequest]
        public HttpResponseMessage PostVoice([FromBody] TwilioVoiceRequest voiceRequest)
        {
            var message =
                "Thanks for calling! " +
                $"Your phone number is {voiceRequest.From}. " +
                "I got your call because of Twilio's webhook. " +
                "Goodbye!";

            var response = new VoiceResponse();
            response.Say(message);
            response.Hangup();

            return ToResponseMessage(response.ToString());
        }

        [Route("message")]
        [AcceptVerbs("POST")]
        [ValidateTwilioRequest]
        public HttpResponseMessage PostMessage([FromBody] TwilioMessagingRequest messagingRequest)
        {
            var message =
                $"Your text to me was {messagingRequest.Body.Length} characters long. " +
                "Webhooks are neat :)";

            var response = new MessagingResponse();
            response.Append(new Message(message));

            return ToResponseMessage(response.ToString());
        }

        private static HttpResponseMessage ToResponseMessage(string response)
        {
            return new HttpResponseMessage
            {
                Content = new StringContent(response, Encoding.UTF8, "application/xml")
            };
        }
    }
}
Apply the request validation filter attribute to a set of ApiController methods

To use the filter attribute with an existing view, just put [ValidateTwilioRequest] above the action’s definition. In this sample application, we use our filter attribute with two controller actions: one that handles incoming phone calls and another that handles incoming text messages.

Configuration Options

You will need to add the following to your Web.config file, in the appSettings section:

    <add key="TwilioAuthToken" value="your_auth_token" />
    <add key="TwilioBaseUrl" value="https://????.ngrok.io"/>

You can get your Twilio Auth Token from the Twilio Console. The TwilioBaseUrl setting should be the public protocol and domain that you have configured on your Twilio phone number. For example, if you are using ngrok, you would put your ngrok URL here. If you are deploying to Azure or another cloud provider, put your publicly accessible domain here and include https or http, as appropriate for your application.

Disable request validation during testing

If you write tests for your controller actions, those tests may fail where you use your Twilio request validation filter. Any requests your test suite sends to those actions will fail the filter’s validation check.

To fix this problem we recommend adding an extra check in your filter attribute, like so, telling it to only reject incoming requests if your app is running in production.

Loading Code Samples...
Language
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Twilio.Security;

namespace ValidateRequestExample.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class ValidateTwilioRequestImprovedAttribute : ActionFilterAttribute
    {
        private readonly string _authToken;
        private readonly string _urlSchemeAndDomain;
        private static bool IsTestEnvironment =>
            bool.Parse(ConfigurationManager.AppSettings["IsTestEnvironment"]);

        public ValidateTwilioRequestImprovedAttribute()
        {
            _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
            _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];
        }

        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            if (!await IsValidRequestAsync(actionContext.Request) && !IsTestEnvironment)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.Forbidden,
                    "The Twilio request is invalid"
                );
            }

            await base.OnActionExecutingAsync(actionContext, cancellationToken);
        }

        private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)
        {
            var headerExists = request.Headers.TryGetValues(
                "X-Twilio-Signature", out IEnumerable<string> signature);
            if (!headerExists) return false;

            var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;
            var formData = await GetFormDataAsync(request.Content);
            return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());
        }

        private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)
        {
            string postData;
            using (var stream = new StreamReader(await content.ReadAsStreamAsync()))
            {
                stream.BaseStream.Position = 0;
                postData = await stream.ReadToEndAsync();
            }

            if (!String.IsNullOrEmpty(postData) && postData.Contains("="))
            {
                return postData.Split('&')
                    .Select(x => x.Split('='))
                    .ToDictionary(
                        x => Uri.UnescapeDataString(x[0]),
                        x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))
                    );
            }

            return new Dictionary<string, string>();
        }
    }
}
Use this version of the custom filter attribute if you test your controllers.
An improved ASP.NET Web API request validation filter attribute, useful for testing

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

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 ASP.NET Web API application in general, check out the security considerations in the official ASP.NET Web API docs.

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.

Loading Code Samples...
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Twilio.Security;

namespace ValidateRequestExample.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class ValidateTwilioRequestAttribute : ActionFilterAttribute
    {
        private readonly string _authToken;
        private readonly string _urlSchemeAndDomain;

        public ValidateTwilioRequestAttribute()
        {
            _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
            _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];
        }

        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            if (!await IsValidRequestAsync(actionContext.Request))
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.Forbidden,
                    "The Twilio request is invalid"
                );
            }

            await base.OnActionExecutingAsync(actionContext, cancellationToken);
        }

        private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)
        {
            var headerExists = request.Headers.TryGetValues(
                "X-Twilio-Signature", out IEnumerable<string> signature);
            if (!headerExists) return false;

            var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;
            var formData = await GetFormDataAsync(request.Content);
            return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());
        }

        private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)
        {
            string postData;
            using (var stream = new StreamReader(await content.ReadAsStreamAsync()))
            {
                stream.BaseStream.Position = 0;
                postData = await stream.ReadToEndAsync();
            }

            if(!String.IsNullOrEmpty(postData) && postData.Contains("="))
            {
                return postData.Split('&')
                    .Select(x => x.Split('='))
                    .ToDictionary(
                        x => Uri.UnescapeDataString(x[0]),
                        x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))
                    );
            }

            return new Dictionary<string, string>();
        }
    }
}
using System.Net.Http;
using System.Text;
using System.Web.Http;
using Twilio.TwiML;
using Twilio.TwiML.Messaging;
using ValidateRequestExample.Filters;

namespace ValidateRequestExample.Controllers
{
    public class TwilioMessagingRequest
    {
        public string Body { get; set; }
    }

    public class TwilioVoiceRequest
    {
        public string From { get; set; }
    }

    public class IncomingController : ApiController
    {
        [Route("voice")]
        [AcceptVerbs("POST")]
        [ValidateTwilioRequest]
        public HttpResponseMessage PostVoice([FromBody] TwilioVoiceRequest voiceRequest)
        {
            var message =
                "Thanks for calling! " +
                $"Your phone number is {voiceRequest.From}. " +
                "I got your call because of Twilio's webhook. " +
                "Goodbye!";

            var response = new VoiceResponse();
            response.Say(message);
            response.Hangup();

            return ToResponseMessage(response.ToString());
        }

        [Route("message")]
        [AcceptVerbs("POST")]
        [ValidateTwilioRequest]
        public HttpResponseMessage PostMessage([FromBody] TwilioMessagingRequest messagingRequest)
        {
            var message =
                $"Your text to me was {messagingRequest.Body.Length} characters long. " +
                "Webhooks are neat :)";

            var response = new MessagingResponse();
            response.Append(new Message(message));

            return ToResponseMessage(response.ToString());
        }

        private static HttpResponseMessage ToResponseMessage(string response)
        {
            return new HttpResponseMessage
            {
                Content = new StringContent(response, Encoding.UTF8, "application/xml")
            };
        }
    }
}
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Twilio.Security;

namespace ValidateRequestExample.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class ValidateTwilioRequestImprovedAttribute : ActionFilterAttribute
    {
        private readonly string _authToken;
        private readonly string _urlSchemeAndDomain;
        private static bool IsTestEnvironment =>
            bool.Parse(ConfigurationManager.AppSettings["IsTestEnvironment"]);

        public ValidateTwilioRequestImprovedAttribute()
        {
            _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
            _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];
        }

        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            if (!await IsValidRequestAsync(actionContext.Request) && !IsTestEnvironment)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.Forbidden,
                    "The Twilio request is invalid"
                );
            }

            await base.OnActionExecutingAsync(actionContext, cancellationToken);
        }

        private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)
        {
            var headerExists = request.Headers.TryGetValues(
                "X-Twilio-Signature", out IEnumerable<string> signature);
            if (!headerExists) return false;

            var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;
            var formData = await GetFormDataAsync(request.Content);
            return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());
        }

        private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)
        {
            string postData;
            using (var stream = new StreamReader(await content.ReadAsStreamAsync()))
            {
                stream.BaseStream.Position = 0;
                postData = await stream.ReadToEndAsync();
            }

            if (!String.IsNullOrEmpty(postData) && postData.Contains("="))
            {
                return postData.Split('&')
                    .Select(x => x.Split('='))
                    .ToDictionary(
                        x => Uri.UnescapeDataString(x[0]),
                        x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))
                    );
            }

            return new Dictionary<string, string>();
        }
    }
}