Allow custom handling of antiforgery failures

To enable custom handling of antiforgery validation failures, use an
`AntiforgeryValidationFailedResult` which is just a `BadRequestResult`
but allows to be identified explicitly inside always-running result
filters using the `IAntiforgeryValidationFailedResult` marker interface.
This commit is contained in:
Patrick Westerhoff 2018-10-18 01:18:45 +02:00 committed by Pranav K
parent 8a183bb4f4
commit ddbe0fef26
7 changed files with 111 additions and 2 deletions

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.Core.Infrastructure;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// A <see cref="BadRequestResult"/> used for antiforgery validation
/// failures. Use <see cref="IAntiforgeryValidationFailedResult"/> to
/// match for validation failures inside MVC result filters.
/// </summary>
public class AntiforgeryValidationFailedResult : BadRequestResult, IAntiforgeryValidationFailedResult
{ }
}

View File

@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Core.Infrastructure
{
/// <summary>
/// Represents an <see cref="IActionResult"/> that is used when the
/// antiforgery validation failed. This can be matched inside MVC result
/// filters to process the validation failure.
/// </summary>
public interface IAntiforgeryValidationFailedResult : IActionResult
{ }
}

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
catch (AntiforgeryValidationException exception)
{
_logger.AntiforgeryTokenInvalid(exception.Message, exception);
context.Result = new BadRequestResult();
context.Result = new AntiforgeryValidationFailedResult();
}
}
}

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -175,5 +174,32 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var pragmaValue = Assert.Single(response.Headers.Pragma.ToArray());
Assert.Equal("no-cache", pragmaValue.Name);
}
[Fact]
public async Task RequestWithoutAntiforgeryToken_SendsBadRequest()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Antiforgery/Login");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task RequestWithoutAntiforgeryToken_ExecutesResultFilter()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Antiforgery/LoginWithRedirectResultFilter");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("http://example.com/antiforgery-redirect", response.Headers.Location.AbsoluteUri);
}
}
}

View File

@ -73,5 +73,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Assert
antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()), Times.Never());
}
[Fact]
public async Task Filter_SetsFailureResult()
{
// Arrange
var antiforgery = new Mock<IAntiforgery>(MockBehavior.Strict);
antiforgery
.Setup(a => a.ValidateRequestAsync(It.IsAny<HttpContext>()))
.Throws(new AntiforgeryValidationException("Failed"))
.Verifiable();
var filter = new ValidateAntiforgeryTokenAuthorizationFilter(antiforgery.Object, NullLoggerFactory.Instance);
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
actionContext.HttpContext.Request.Method = "POST";
var context = new AuthorizationFilterContext(actionContext, new[] { filter });
// Act
await filter.OnAuthorizationAsync(context);
// Assert
Assert.IsType<AntiforgeryValidationFailedResult>(context.Result);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BasicWebSite.Filters;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -39,6 +40,16 @@ namespace BasicWebSite.Controllers
return "OK";
}
// POST: /Antiforgery/LoginWithRedirectResultFilter
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[TypeFilter(typeof(RedirectAntiforgeryValidationFailedResultFilter))]
public string LoginWithRedirectResultFilter(LoginViewModel model)
{
return "Ok";
}
// GET: /Antiforgery/FlushAsyncLogin
[AllowAnonymous]
public ActionResult FlushAsyncLogin(string returnUrl = null)

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Core.Infrastructure;
using Microsoft.AspNetCore.Mvc.Filters;
namespace BasicWebSite.Filters
{
public class RedirectAntiforgeryValidationFailedResultFilter : IAlwaysRunResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is IAntiforgeryValidationFailedResult result)
{
context.Result = new RedirectResult("http://example.com/antiforgery-redirect");
}
}
public void OnResultExecuted(ResultExecutedContext context)
{ }
}
}