From ddbe0fef2617f94a42312f002860f8f45145f11d Mon Sep 17 00:00:00 2001 From: Patrick Westerhoff Date: Thu, 18 Oct 2018 01:18:45 +0200 Subject: [PATCH] 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. --- .../AntiforgeryValidationFailedResult.cs | 15 ++++++++++ .../IAntiforgeryValidationFailedResult.cs | 13 +++++++++ ...dateAntiforgeryTokenAuthorizationFilter.cs | 2 +- .../AntiforgeryTests.cs | 28 ++++++++++++++++++- ...AntiforgeryTokenAuthorizationFilterTest.cs | 24 ++++++++++++++++ .../Controllers/AntiforgeryController.cs | 11 ++++++++ ...AntiforgeryValidationFailedResultFilter.cs | 20 +++++++++++++ 7 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/AntiforgeryValidationFailedResult.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IAntiforgeryValidationFailedResult.cs create mode 100644 test/WebSites/BasicWebSite/Filters/RedirectAntiforgeryValidationFailedResultFilter.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AntiforgeryValidationFailedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AntiforgeryValidationFailedResult.cs new file mode 100644 index 0000000000..95e0643127 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/AntiforgeryValidationFailedResult.cs @@ -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 +{ + /// + /// A used for antiforgery validation + /// failures. Use to + /// match for validation failures inside MVC result filters. + /// + public class AntiforgeryValidationFailedResult : BadRequestResult, IAntiforgeryValidationFailedResult + { } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IAntiforgeryValidationFailedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IAntiforgeryValidationFailedResult.cs new file mode 100644 index 0000000000..07befb4452 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IAntiforgeryValidationFailedResult.cs @@ -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 +{ + /// + /// Represents an that is used when the + /// antiforgery validation failed. This can be matched inside MVC result + /// filters to process the validation failure. + /// + public interface IAntiforgeryValidationFailedResult : IActionResult + { } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidateAntiforgeryTokenAuthorizationFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidateAntiforgeryTokenAuthorizationFilter.cs index 7225eb76c1..901ea36cf8 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidateAntiforgeryTokenAuthorizationFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidateAntiforgeryTokenAuthorizationFilter.cs @@ -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(); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs index 78fc00bec6..746f949205 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTests.cs @@ -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); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ValidateAntiforgeryTokenAuthorizationFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ValidateAntiforgeryTokenAuthorizationFilterTest.cs index 2748716667..d8e62d278e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ValidateAntiforgeryTokenAuthorizationFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ValidateAntiforgeryTokenAuthorizationFilterTest.cs @@ -73,5 +73,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Assert antiforgery.Verify(a => a.ValidateRequestAsync(It.IsAny()), Times.Never()); } + + [Fact] + public async Task Filter_SetsFailureResult() + { + // Arrange + var antiforgery = new Mock(MockBehavior.Strict); + antiforgery + .Setup(a => a.ValidateRequestAsync(It.IsAny())) + .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(context.Result); + } } } diff --git a/test/WebSites/BasicWebSite/Controllers/AntiforgeryController.cs b/test/WebSites/BasicWebSite/Controllers/AntiforgeryController.cs index 7da1b783c2..415a34ba46 100644 --- a/test/WebSites/BasicWebSite/Controllers/AntiforgeryController.cs +++ b/test/WebSites/BasicWebSite/Controllers/AntiforgeryController.cs @@ -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) diff --git a/test/WebSites/BasicWebSite/Filters/RedirectAntiforgeryValidationFailedResultFilter.cs b/test/WebSites/BasicWebSite/Filters/RedirectAntiforgeryValidationFailedResultFilter.cs new file mode 100644 index 0000000000..ca7c61781b --- /dev/null +++ b/test/WebSites/BasicWebSite/Filters/RedirectAntiforgeryValidationFailedResultFilter.cs @@ -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) + { } + } +}