I have implemented a AuthorizationHandler
according to an official Twilio tutorial but it only works for SMS-related requests but not voice-related requests (always fail the validation).
Below is the one and only AuthorizationHandler
applied to different controllers that accept POST request from Twilio to notify my API of inbound and outbound voice calls, inbound SMS, and status change to outbound SMS:
public class TwilioInboundRequestAuthorizationHandler : AuthorizationHandler<TwilioInboundRequestRequirement>
{
private readonly RequestValidator _requestValidator;
public TwilioInboundRequestAuthorizationHandler(IOptionsSnapshot<AppOptions> options)
{
// Initialize the validator
_requestValidator = new RequestValidator(options.Value.TwilioAuthToken);
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TwilioInboundRequestRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext mvcContext)
{
// Examine MVC-specific things like routing data.
HttpRequest httpRequest = mvcContext.HttpContext.Request;
if (IsValidRequest(httpRequest))
{
context.Succeed(requirement);
}
else
{
/* Omitted some code that logs the error to a cloud service */
context.Fail();
}
}
else
{
throw new NotImplementedException();
}
// Check if the requirement is fulfilled.
return Task.CompletedTask;
}
private bool IsValidRequest(HttpRequest request) {
// The Twilio request URL
var requestUrl = RequestRawUrl(request);
var parameters = ToDictionary(request.Form);
// The X-Twilio-Signature header attached to the request
var signature = request.Headers["X-Twilio-Signature"];
return _requestValidator.Validate(requestUrl, parameters, signature);
}
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
}
private static IDictionary<string, string> ToDictionary(IFormCollection collection)
{
return collection.Keys
.Select(key => new { Key = key, Value = collection[key] })
.ToDictionary(p => p.Key, p => p.Value.ToString());
}
}
public class TwilioInboundRequestRequirement : IAuthorizationRequirement
{
}
EDIT:
According to a suggestion from Twilio Support, I should change the RequestRawUrl
to strip away the port number from the URL. However, that causes the validation working for voice calls only, while for SMS it doesn't work anymore (opposite to the original issue). I suspect Twilio has been setting an incorrect signature in the request header for either voice or SMS.
I changed the RequestRawUrl
function from
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
}
to
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host.Host}{request.Path}{request.QueryString}";
}