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!
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.
Confirm incoming requests to your controllers are genuine with this filter.
_74using System;_74using System.Collections.Generic;_74using System.Configuration;_74using System.IO;_74using System.Linq;_74using System.Net;_74using System.Net.Http;_74using System.Threading;_74using System.Threading.Tasks;_74using System.Web.Http.Controllers;_74using System.Web.Http.Filters;_74using Twilio.Security;_74_74namespace ValidateRequestExample.Filters_74{_74 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]_74 public class ValidateTwilioRequestAttribute : ActionFilterAttribute_74 {_74 private readonly string _authToken;_74 private readonly string _urlSchemeAndDomain;_74_74 public ValidateTwilioRequestAttribute()_74 {_74 _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];_74 _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];_74 }_74_74 public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)_74 {_74 if (!await IsValidRequestAsync(actionContext.Request))_74 {_74 actionContext.Response = actionContext.Request.CreateErrorResponse(_74 HttpStatusCode.Forbidden,_74 "The Twilio request is invalid"_74 );_74 }_74_74 await base.OnActionExecutingAsync(actionContext, cancellationToken);_74 }_74_74 private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)_74 {_74 var headerExists = request.Headers.TryGetValues(_74 "X-Twilio-Signature", out IEnumerable<string> signature);_74 if (!headerExists) return false;_74_74 var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;_74 var formData = await GetFormDataAsync(request.Content);_74 return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());_74 }_74_74 private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)_74 {_74 string postData;_74 using (var stream = new StreamReader(await content.ReadAsStreamAsync()))_74 {_74 stream.BaseStream.Position = 0;_74 postData = await stream.ReadToEndAsync();_74 }_74_74 if(!String.IsNullOrEmpty(postData) && postData.Contains("="))_74 {_74 return postData.Split('&')_74 .Select(x => x.Split('='))_74 .ToDictionary(_74 x => Uri.UnescapeDataString(x[0]),_74 x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))_74 );_74 }_74_74 return new Dictionary<string, string>();_74 }_74 }_74}
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 call 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.
Now we're ready to apply our filter attribute to any controller action in our ASP.NET application that handles incoming requests from Twilio.
_63using System.Net.Http;_63using System.Text;_63using System.Web.Http;_63using Twilio.TwiML;_63using Twilio.TwiML.Messaging;_63using ValidateRequestExample.Filters;_63_63namespace ValidateRequestExample.Controllers_63{_63 public class TwilioMessagingRequest_63 {_63 public string Body { get; set; }_63 }_63_63 public class TwilioVoiceRequest_63 {_63 public string From { get; set; }_63 }_63_63 public class IncomingController : ApiController_63 {_63 [Route("voice")]_63 [AcceptVerbs("POST")]_63 [ValidateTwilioRequest]_63 public HttpResponseMessage PostVoice([FromBody] TwilioVoiceRequest voiceRequest)_63 {_63 var message =_63 "Thanks for calling! " +_63 $"Your phone number is {voiceRequest.From}. " +_63 "I got your call because of Twilio's webhook. " +_63 "Goodbye!";_63_63 var response = new VoiceResponse();_63 response.Say(message);_63 response.Hangup();_63_63 return ToResponseMessage(response.ToString());_63 }_63_63 [Route("message")]_63 [AcceptVerbs("POST")]_63 [ValidateTwilioRequest]_63 public HttpResponseMessage PostMessage([FromBody] TwilioMessagingRequest messagingRequest)_63 {_63 var message =_63 $"Your text to me was {messagingRequest.Body.Length} characters long. " +_63 "Webhooks are neat :)";_63_63 var response = new MessagingResponse();_63 response.Append(new Message(message));_63_63 return ToResponseMessage(response.ToString());_63 }_63_63 private static HttpResponseMessage ToResponseMessage(string response)_63 {_63 return new HttpResponseMessage_63 {_63 Content = new StringContent(response, Encoding.UTF8, "application/xml")_63 };_63 }_63 }_63}
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.
You will need to add the following to your Web.config
file, in the appSettings
section:
_10 <add key="TwilioAuthToken" value="your_auth_token" />_10 <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.
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.
Use this version of the custom filter attribute if you test your controllers.
_76using System;_76using System.Collections.Generic;_76using System.Configuration;_76using System.IO;_76using System.Linq;_76using System.Net;_76using System.Net.Http;_76using System.Threading;_76using System.Threading.Tasks;_76using System.Web.Http.Controllers;_76using System.Web.Http.Filters;_76using Twilio.Security;_76_76namespace ValidateRequestExample.Filters_76{_76 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]_76 public class ValidateTwilioRequestImprovedAttribute : ActionFilterAttribute_76 {_76 private readonly string _authToken;_76 private readonly string _urlSchemeAndDomain;_76 private static bool IsTestEnvironment =>_76 bool.Parse(ConfigurationManager.AppSettings["IsTestEnvironment"]);_76_76 public ValidateTwilioRequestImprovedAttribute()_76 {_76 _authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];_76 _urlSchemeAndDomain = ConfigurationManager.AppSettings["TwilioBaseUrl"];_76 }_76_76 public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)_76 {_76 if (!await IsValidRequestAsync(actionContext.Request) && !IsTestEnvironment)_76 {_76 actionContext.Response = actionContext.Request.CreateErrorResponse(_76 HttpStatusCode.Forbidden,_76 "The Twilio request is invalid"_76 );_76 }_76_76 await base.OnActionExecutingAsync(actionContext, cancellationToken);_76 }_76_76 private async Task<bool> IsValidRequestAsync(HttpRequestMessage request)_76 {_76 var headerExists = request.Headers.TryGetValues(_76 "X-Twilio-Signature", out IEnumerable<string> signature);_76 if (!headerExists) return false;_76_76 var requestUrl = _urlSchemeAndDomain + request.RequestUri.PathAndQuery;_76 var formData = await GetFormDataAsync(request.Content);_76 return new RequestValidator(_authToken).Validate(requestUrl, formData, signature.First());_76 }_76_76 private async Task<IDictionary<string, string>> GetFormDataAsync(HttpContent content)_76 {_76 string postData;_76 using (var stream = new StreamReader(await content.ReadAsStreamAsync()))_76 {_76 stream.BaseStream.Position = 0;_76 postData = await stream.ReadToEndAsync();_76 }_76_76 if (!String.IsNullOrEmpty(postData) && postData.Contains("="))_76 {_76 return postData.Split('&')_76 .Select(x => x.Split('='))_76 .ToDictionary(_76 x => Uri.UnescapeDataString(x[0]),_76 x => Uri.UnescapeDataString(x[1].Replace("+", "%20"))_76 );_76 }_76_76 return new Dictionary<string, string>();_76 }_76 }_76}
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.