1
votes

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}";
}
3
Is there any other difference between the URLs you are using for voice and SMS webhooks?philnash
@philnash nope - other than them being in different controllersKelvin Lai
If it works for SMS then it should work for voice too, there's nothing different in the algorithm. I'd be looking for any other differences between how it's implemented, whether there's some middleware on one controller and not another, are all the parameters making it through to the voice controller?philnash
@philnash see editKelvin Lai
Do you have the URL set differently in the Twilio console for SMS and calls?philnash

3 Answers

2
votes

We had a similar problem. What it boiled down to was that for some methods, (like SMS) the request URL comes in as https, and other methods (like TwiML voice script callbacks) the request comes in written as http.

If the request comes in written as http, the .Validate(...) method will fail on you, even though it is a valid request.

So to get the Twilio request validator to work, we simply rewrite the request URL.

    private bool IsValidRequest(HttpRequestBase request)
    {
        var signature = request.Headers["X-Twilio-Signature"];
        Debug.WriteLine(request.Headers["X-Twilio-Signature"]);
        var requestUrl = rewriteUri(request.Url.AbsoluteUri);
        Debug.WriteLine("URI is: " + rewriteUri(request.Url.AbsoluteUri));

        return _requestValidator.Validate(requestUrl, request.Form, signature);
    }

    private string rewriteUri(string absoluteUri)
    {
        //check to make sure we're not replacing 'https' with 'httpss'
        if (!absoluteUri.Contains("https"))
        {
            return Regex.Replace(absoluteUri, @"http", "https");
        }
        return absoluteUri;
    }
0
votes

I was having the exact same issue when running locally behind ngrok, but not when deployed. I figured out that the RequestValidator expects the original ngrok URL, but by default we are passing localhost. I ended up solving it like so:

    private bool IsValidRequest(HttpRequest request)
    {
        var requestUrl = RequestRawUrl(request);
        var parameters = ToDictionary(request.Form);
        var signature = request.Headers["X-Twilio-Signature"];

        // Check if we are running locally and need to pass ngrok through for validation to succeed.
        if (request.Headers.ContainsKey("X-Original-Host") && request.Headers["X-Original-Host"][0].Contains("ngrok"))
        {
            requestUrl = requestUrl.Replace(request.Headers["Host"][0], request.Headers["X-Original-Host"][0]);
        }

        return _requestValidator.Validate(requestUrl, parameters, signature);
    }
-1
votes

If you using ngrok, verifier expects ngrok URL (i think same thing with other proxies) In PHP the original host can be get from HTTP_X_ORIGINAL_HOST, maybe this would help you somehow