Skip to content

Commit

Permalink
Merge pull request #114 from twilio-labs/validate-async-form
Browse files Browse the repository at this point in the history
Make Request Validation async to load the form async
  • Loading branch information
Swimburger authored Mar 2, 2023
2 parents 08f8b94 + 7304822 commit c63c585
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 25 deletions.
6 changes: 5 additions & 1 deletion src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Twilio.AspNet.Core.UnitTests;
Expand Down Expand Up @@ -42,7 +44,9 @@ public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null
if (form != null)
{
Request.Setup(x => x.Method).Returns("POST");
Request.Setup(x => x.Form).Returns(form);
Request.Setup(x => x.Form).Returns(form);
Request.Setup(x => x.ReadFormAsync(new CancellationToken()))
.Returns(() => Task.FromResult((IFormCollection)form));
Request.Setup(x => x.HasFormContentType).Returns(true);
}
}
Expand Down
60 changes: 52 additions & 8 deletions src/Twilio.AspNet.Core/RequestValidationHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand All @@ -18,23 +20,63 @@ public static class RequestValidationHelper
/// the ASP.NET MVC ValidateRequestAttribute
/// </summary>
/// <param name="context">HttpContext to use for validation</param>
internal static bool IsValidRequest(HttpContext context)
internal static async Task<bool> IsValidRequestAsync(HttpContext context)
{
var options = context.RequestServices
.GetRequiredService<IOptionsSnapshot<TwilioRequestValidationOptions>>().Value;

var authToken = options.AuthToken;
var baseUrlOverride = options.BaseUrlOverride;
var allowLocal = options.AllowLocal;

var request = context.Request;

string urlOverride = null;
if (!string.IsNullOrEmpty(baseUrlOverride))
{
urlOverride = $"{baseUrlOverride}{request.Path}{request.QueryString}";
}

return await IsValidRequestAsync(context, authToken, urlOverride, allowLocal).ConfigureAwait(false);
}

/// <summary>
/// Performs request validation using the current HTTP context passed in manually or from
/// the ASP.NET MVC ValidateRequestAttribute
/// </summary>
/// <param name="context">HttpContext to use for validation</param>
/// <param name="authToken">AuthToken for the account used to sign the request</param>
/// <param name="allowLocal">
/// Skip validation for local requests.
/// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery.
/// </param>
public static Task<bool> IsValidRequestAsync(HttpContext context, string authToken, bool allowLocal = false)
=> IsValidRequestAsync(context, authToken, null, allowLocal);

/// <summary>
/// Performs request validation using the current HTTP context passed in manually or from
/// the ASP.NET MVC ValidateRequestAttribute
/// </summary>
/// <param name="context">HttpContext to use for validation</param>
/// <param name="authToken">AuthToken for the account used to sign the request</param>
/// <param name="urlOverride">The URL to use for validation, if different from Request.Url (sometimes needed if web site is behind a proxy or load-balancer)</param>
/// <param name="allowLocal">
/// Skip validation for local requests.
/// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery.
/// </param>
public static async Task<bool> IsValidRequestAsync(
HttpContext context,
string authToken,
string urlOverride,
bool allowLocal = false
)
{
if (context.Request.HasFormContentType)
{
// this will load the form async, but then cache is in context.Request.Form which is used later
await context.Request.ReadFormAsync(context.RequestAborted).ConfigureAwait(false);
}

return IsValidRequest(context, authToken, urlOverride, allowLocal);
}

Expand Down Expand Up @@ -83,9 +125,11 @@ public static bool IsValidRequest(
? $"{request.Scheme}://{(request.IsHttps ? request.Host.Host : request.Host.ToUriComponent())}{request.Path}{request.QueryString}"
: urlOverride;

var parameters = request.HasFormContentType
? request.Form.Keys.ToDictionary(k => k, k => request.Form[k].ToString())
: null;
Dictionary<string, string> parameters = null;
if (request.HasFormContentType)
{
parameters = request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
}

var validator = new RequestValidator(authToken);
return validator.Validate(
Expand Down
4 changes: 2 additions & 2 deletions src/Twilio.AspNet.Core/Twilio.AspNet.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net5.0;net6.0;net7.0</TargetFrameworks>
<OutputType>Library</OutputType>
Expand Down Expand Up @@ -30,7 +30,7 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Twilio" Version="6.*" />
<PackageReference Include="Twilio" Version="6.2.0" />
<PackageReference Include="Twilio.AspNet.Common" Version="0.0.0-alpha" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
Expand Down
19 changes: 13 additions & 6 deletions src/Twilio.AspNet.Core/ValidateRequestAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Net;
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

Expand All @@ -7,17 +9,22 @@ namespace Twilio.AspNet.Core
/// <summary>
/// Represents an attribute that is used to prevent forgery of a request.
/// </summary>
public class ValidateRequestAttribute : ActionFilterAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ValidateRequestAttribute : Attribute, IAsyncActionFilter
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
public async Task OnActionExecutionAsync(
ActionExecutingContext filterContext,
ActionExecutionDelegate next
)
{
var context = filterContext.HttpContext;
if (!RequestValidationHelper.IsValidRequest(context))
if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false))
{
filterContext.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden);
await next();
return;
}

base.OnActionExecuting(filterContext);
filterContext.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden);
}
}
}
6 changes: 3 additions & 3 deletions src/Twilio.AspNet.Core/ValidateTwilioRequestFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ public async ValueTask<object> InvokeAsync(
EndpointFilterDelegate next
)
{
if (RequestValidationHelper.IsValidRequest(efiContext.HttpContext))
if (await RequestValidationHelper.IsValidRequestAsync(efiContext.HttpContext).ConfigureAwait(false))
{
return await next(efiContext);
}

return Results.StatusCode((int) HttpStatusCode.Forbidden);
return Results.StatusCode((int)HttpStatusCode.Forbidden);
}
}

Expand All @@ -34,7 +34,7 @@ public static class TwilioFilterExtensions
/// <returns></returns>
public static RouteHandlerBuilder ValidateTwilioRequest(this RouteHandlerBuilder builder)
=> builder.AddEndpointFilter<ValidateTwilioRequestFilter>();

/// <summary>
/// Validates that incoming HTTP request originates from Twilio.
/// </summary>
Expand Down
8 changes: 4 additions & 4 deletions src/Twilio.AspNet.Core/ValidateTwilioRequestMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ public ValidateTwilioRequestMiddleware(RequestDelegate next)

public async Task InvokeAsync(HttpContext context)
{
if (!RequestValidationHelper.IsValidRequest(context))
if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false))
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
await next(context);
return;
}

await next(context);
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Twilio.AspNet.Mvc/Twilio.AspNet.Mvc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Twilio" Version="6.*" />
<PackageReference Include="Twilio" Version="6.2.0" />
<PackageReference Include="Twilio.AspNet.Common" Version="0.0.0-alpha" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
Expand Down
1 change: 1 addition & 0 deletions src/Twilio.AspNet.Mvc/ValidateRequestAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Twilio.AspNet.Mvc
/// <summary>
/// Represents an attribute that is used to prevent forgery of a request.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Module)]
public class ValidateRequestAttribute : ActionFilterAttribute
{
protected internal string AuthToken { get; set; }
Expand Down

0 comments on commit c63c585

Please sign in to comment.