Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

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


(warning)

Warning

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

create-a-custom-filter-attribute page anchor

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

ASP.NET Web API filter attribute to validate Twilio requests

aspnet-web-api-filter-attribute-to-validate-twilio-requests page anchor

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


_74
using System;
_74
using System.Collections.Generic;
_74
using System.Configuration;
_74
using System.IO;
_74
using System.Linq;
_74
using System.Net;
_74
using System.Net.Http;
_74
using System.Threading;
_74
using System.Threading.Tasks;
_74
using System.Web.Http.Controllers;
_74
using System.Web.Http.Filters;
_74
using Twilio.Security;
_74
_74
namespace 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.


Use the filter attribute with our Twilio webhooks

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

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

Apply the request validation filter attribute to a set of ApiController methods

apply-the-request-validation-filter-attribute-to-a-set-of-apicontroller-methods page anchor

_63
using System.Net.Http;
_63
using System.Text;
_63
using System.Web.Http;
_63
using Twilio.TwiML;
_63
using Twilio.TwiML.Messaging;
_63
using ValidateRequestExample.Filters;
_63
_63
namespace 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.

Configuration Options

configuration-options page anchor

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

disable-request-validation-during-testing page anchor

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.

An improved ASP.NET Web API request validation filter attribute, useful for testing

an-improved-aspnet-web-api-request-validation-filter-attribute-useful-for-testing page anchor

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


_76
using System;
_76
using System.Collections.Generic;
_76
using System.Configuration;
_76
using System.IO;
_76
using System.Linq;
_76
using System.Net;
_76
using System.Net.Http;
_76
using System.Threading;
_76
using System.Threading.Tasks;
_76
using System.Web.Http.Controllers;
_76
using System.Web.Http.Filters;
_76
using Twilio.Security;
_76
_76
namespace 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(link takes you to an external page).


Rate this page: