In this guide, we'll cover how to secure your C# / ASP.NET Core 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!
If you don't want to develop your own validation filter, you can install the Twilio helper library for ASP.NET Core and use the library's [ValidateRequest]
attribute instead that has more features. This library also contains an endpoint filter and a middleware validator.
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 controllers and actions that accept incoming requests from Twilio.
Confirm incoming requests to your controllers are genuine with this filter.
_57using System;_57using System.Collections.Generic;_57using System.Linq;_57using System.Threading.Tasks;_57using Microsoft.AspNetCore.Http;_57using Microsoft.AspNetCore.Mvc;_57using Microsoft.AspNetCore.Mvc.Filters;_57using Microsoft.Extensions.Configuration;_57using Twilio.Security;_57_57namespace ValidateRequestExample.Filters_57{_57 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]_57 public class ValidateTwilioRequestAttribute : TypeFilterAttribute_57 {_57 public ValidateTwilioRequestAttribute() : base(typeof(ValidateTwilioRequestFilter))_57 {_57 }_57 }_57_57 internal class ValidateTwilioRequestFilter : IAsyncActionFilter_57 {_57 private readonly RequestValidator _requestValidator;_57_57 public ValidateTwilioRequestFilter(IConfiguration configuration)_57 {_57 var authToken = configuration["Twilio:AuthToken"] ?? throw new Exception("'Twilio:AuthToken' not configured.");_57 _requestValidator = new RequestValidator(authToken);_57 }_57_57 public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)_57 {_57 var httpContext = context.HttpContext;_57 var request = httpContext.Request;_57_57 var requestUrl = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";_57 Dictionary<string, string> parameters = null;_57_57 if (request.HasFormContentType)_57 {_57 var form = await request.ReadFormAsync(httpContext.RequestAborted).ConfigureAwait(false);_57 parameters = form.ToDictionary(p => p.Key, p => p.Value.ToString());_57 }_57_57 var signature = request.Headers["X-Twilio-Signature"];_57 var isValid = _requestValidator.Validate(requestUrl, parameters, signature);_57_57 if (!isValid)_57 {_57 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;_57 return;_57 }_57_57 await next();_57 }_57 }_57}
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 forbidden 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.
Apply a custom Twilio request validation filter attribute to a set of controller methods used for Twilio webhooks.
_37using Microsoft.AspNetCore.Mvc;_37using Twilio.TwiML;_37using ValidateRequestExample.Filters;_37_37namespace ValidateRequestExample.Controllers_37{_37 [Route("[controller]/[action]")]_37 public class IncomingController : Controller_37 {_37 [ValidateTwilioRequest]_37 public IActionResult Voice(string from)_37 {_37 var message = "Thanks for calling! " +_37 $"Your phone number is {from}. " +_37 "I got your call because of Twilio\'s webhook. " +_37 "Goodbye!";_37_37 var response = new VoiceResponse();_37 response.Say(string.Format(message, from));_37 response.Hangup();_37_37 return Content(response.ToString(), "text/xml");_37 }_37_37 [ValidateTwilioRequest]_37 public IActionResult Message(string body)_37 {_37 var message = $"Your text to me was {body.Length} characters long. " +_37 "Webhooks are neat :)";_37_37 var response = new MessagingResponse();_37 response.Message(message);_37_37 return Content(response.ToString(), "text/xml");_37 }_37 }_37}
To use the filter attribute with an existing controller action, 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.
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 ASP.NET 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 method will need to reconstruct the request's original URL using request headers like X-Original-Host
and X-Forwarded-Proto
, if available.
Before running the application, make sure you configure your Twilio Auth Token as the Twilio:AuthToken
configuration, using .NET's secrets manager, environment variables, a vault service, or some other secure configuration source.
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.
_66using System;_66using System.Collections.Generic;_66using System.Linq;_66using System.Threading.Tasks;_66using Microsoft.AspNetCore.Hosting;_66using Microsoft.AspNetCore.Http;_66using Microsoft.AspNetCore.Mvc;_66using Microsoft.AspNetCore.Mvc.Filters;_66using Microsoft.Extensions.Configuration;_66using Twilio.Security;_66_66namespace ValidateRequestExample.Filters_66{_66 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]_66 public class ValidateTwilioRequestAttribute : TypeFilterAttribute_66 {_66 public ValidateTwilioRequestAttribute() : base(typeof(ValidateTwilioRequestFilter))_66 {_66 }_66 }_66_66 internal class ValidateTwilioRequestFilter : IAsyncActionFilter_66 {_66 private readonly RequestValidator _requestValidator;_66 private readonly bool _isEnabled;_66_66 public ValidateTwilioRequestFilter(IConfiguration configuration, IWebHostEnvironment environment)_66 {_66 var authToken = configuration["Twilio:AuthToken"] ?? throw new Exception("'Twilio:AuthToken' not configured.");_66 _requestValidator = new RequestValidator(authToken);_66 _isEnabled = configuration.GetValue("Twilio:RequestValidation:Enabled", true);_66 }_66_66 public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)_66 {_66 if (!_isEnabled)_66 {_66 await next();_66 return;_66 }_66_66 var httpContext = context.HttpContext;_66 var request = httpContext.Request;_66_66 var requestUrl = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";_66 Dictionary<string, string> parameters = null;_66_66 if (request.HasFormContentType)_66 {_66 var form = await request.ReadFormAsync(httpContext.RequestAborted).ConfigureAwait(false);_66 parameters = form.ToDictionary(p => p.Key, p => p.Value.ToString());_66 }_66_66 var signature = request.Headers["X-Twilio-Signature"];_66 var isValid = _requestValidator.Validate(requestUrl, parameters, signature);_66_66 if (!isValid)_66 {_66 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;_66 return;_66 }_66_66 await next();_66 }_66 }_66}
To disable the request validation, you can now configure Twilio:RequestValidation:Enabled
to false
in your appsettings.json or appsettings.Development.json file.
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 MVC application in general, check out the security considerations page in the official ASP.NET docs.