diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Authorization/AuthorizeFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Authorization/AuthorizeFilter.cs index abf76ea191..727ac3d659 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Authorization/AuthorizeFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Authorization/AuthorizeFilter.cs @@ -189,7 +189,7 @@ public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext contex var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // Allow Anonymous skips all authorization - if (context.Filters.Any(item => item is IAllowAnonymousFilter)) + if (HasAllowAnonymous(context.Filters)) { return; } @@ -218,5 +218,18 @@ IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider) var policyProvider = serviceProvider.GetRequiredService(); return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData); } + + private static bool HasAllowAnonymous(IList filters) + { + for (var i = 0; i < filters.Count; i++) + { + if (filters[i] is IAllowAnonymousFilter) + { + return true; + } + } + + return false; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/AutoValidateAntiforgeryPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/AutoValidateAntiforgeryPageApplicationModelProvider.cs new file mode 100644 index 0000000000..5503ba5d03 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/AutoValidateAntiforgeryPageApplicationModelProvider.cs @@ -0,0 +1,41 @@ +// 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 System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + internal class AutoValidateAntiforgeryPageApplicationModelProvider : IPageApplicationModelProvider + { + // The order is set to execute after the DefaultPageApplicationModelProvider. + public int Order => -1000 + 10; + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var pageApplicationModel = context.PageApplicationModel; + + // ValidateAntiforgeryTokenAttribute relies on order to determine if it's the effective policy. + // When two antiforgery filters of the same order are added to the application model, the effective policy is determined + // by whatever appears later in the list (closest to the action). This causes filters listed on the model to be pre-empted + // by the one added here. We'll resolve this unusual behavior by skipping the addition of the AutoValidateAntiforgeryTokenAttribute + // when another already exists. + if (!pageApplicationModel.Filters.OfType().Any()) + { + // Always require an antiforgery token on post + pageApplicationModel.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs deleted file mode 100644 index 2103a4606b..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 System; -using Microsoft.AspNetCore.Mvc.ApplicationModels; - -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal -{ - public class AutoValidateAntiforgeryPageApplicationModelProvider : IPageApplicationModelProvider - { - // The order is set to execute after the DefaultPageApplicationModelProvider. - public int Order => -1000 + 10; - - public void OnProvidersExecuted(PageApplicationModelProviderContext context) - { - } - - public void OnProvidersExecuting(PageApplicationModelProviderContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var pageApplicationModel = context.PageApplicationModel; - - // Always require an antiforgery token on post - pageApplicationModel.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs index d7310e628b..17ff0bb735 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs @@ -2,37 +2,44 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.IO; using System.Linq; using System.Net.Http; -using System.Xml.Linq; +using AngleSharp.Dom.Html; +using AngleSharp.Parser.Html; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public static class AntiforgeryTestHelper { + public static string RetrieveAntiforgeryToken(string htmlContent) + => RetrieveAntiforgeryToken(htmlContent, actionUrl: string.Empty); + public static string RetrieveAntiforgeryToken(string htmlContent, string actionUrl) { - htmlContent = "" + htmlContent + ""; - var reader = new StringReader(htmlContent); - var htmlDocument = XDocument.Load(reader); + var parser = new HtmlParser(); + var htmlDocument = parser.Parse(htmlContent); + + return RetrieveAntiforgeryToken(htmlDocument); + } - foreach (var form in htmlDocument.Descendants("form")) + public static string RetrieveAntiforgeryToken(IHtmlDocument htmlDocument) + { + var hiddenInputs = htmlDocument.QuerySelectorAll("form input[type=hidden]"); + foreach (var input in hiddenInputs) { - foreach (var input in form.Descendants("input")) + if (!input.HasAttribute("name")) + { + continue; + } + + var name = input.GetAttribute("name"); + if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]") { - if (input.Attribute("name") != null && - input.Attribute("type") != null && - input.Attribute("type").Value == "hidden" && - (input.Attribute("name").Value == "__RequestVerificationToken" || - input.Attribute("name").Value == "HtmlEncode[[__RequestVerificationToken]]")) - { - return input.Attributes("value").First().Value; - } + return input.GetAttribute("value"); } } - throw new Exception($"Antiforgery token could not be located in {htmlContent}."); + throw new Exception($"Antiforgery token could not be located in {htmlDocument.TextContent}."); } public static CookieMetadata RetrieveAntiforgeryCookie(HttpResponseMessage response) diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 680af29004..6d43c53c97 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -552,5 +552,72 @@ public async Task ViewDataProperties_SetInPageModel_AreTransferredToViewComponen var title = document.QuerySelector("title").TextContent; Assert.Equal("View Data in Pages", title); } + + [Fact] + public async Task Antiforgery_RequestWithoutAntiforgeryToken_Returns200ForHeadRequests() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Head, "/Antiforgery/AntiforgeryDefault"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Antiforgery_RequestWithoutAntiforgeryToken_Returns400BadRequest() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Post, "/Antiforgery/AntiforgeryDefault"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Antiforgery_RequestWithAntiforgeryToken_Succeeds() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Post, "/Antiforgery/AntiforgeryDefault"); + await AddAntiforgeryHeadersAsync(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Antiforgery_IgnoreAntiforgeryTokenAppliedToModelWorks() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Post, "/Antiforgery/IgnoreAntiforgery"); + await AddAntiforgeryHeadersAsync(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + private async Task AddAntiforgeryHeadersAsync(HttpRequestMessage request) + { + var response = await Client.GetAsync(request.RequestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var responseBody = await response.Content.ReadAsStringAsync(); + var formToken = AntiforgeryTestHelper.RetrieveAntiforgeryToken(responseBody); + var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response); + + request.Headers.Add("Cookie", cookie.Key + "=" + cookie.Value); + request.Headers.Add("RequestVerificationToken", formToken); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/AutoValidateAntiforgeryPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/AutoValidateAntiforgeryPageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..f3222ce36c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/AutoValidateAntiforgeryPageApplicationModelProviderTest.cs @@ -0,0 +1,89 @@ +// 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 System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class AutoValidateAntiforgeryPageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_AddsFiltersToModel() + { + // Arrange + var actionDescriptor = new PageActionDescriptor(); + var applicationModel = new PageApplicationModel( + actionDescriptor, + typeof(object).GetTypeInfo(), + new object[0]); + var applicationModelProvider = new AutoValidateAntiforgeryPageApplicationModelProvider(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeof(object).GetTypeInfo()) + { + PageApplicationModel = applicationModel, + }; + + // Act + applicationModelProvider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + applicationModel.Filters, + filter => Assert.IsType(filter)); + } + + [Fact] + public void OnProvidersExecuting_DoesNotAddAutoValidateAntiforgeryTokenAttribute_IfIgnoreAntiforgeryTokenAttributeExists() + { + // Arrange + var expected = new IgnoreAntiforgeryTokenAttribute(); + + var descriptor = new PageActionDescriptor(); + var provider = new AutoValidateAntiforgeryPageApplicationModelProvider(); + var context = new PageApplicationModelProviderContext(descriptor, typeof(object).GetTypeInfo()) + { + PageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()) + { + Filters = { expected }, + }, + }; + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + actual => Assert.Same(expected, actual)); + } + + [Fact] + public void OnProvidersExecuting_DoesNotAddAutoValidateAntiforgeryTokenAttribute_IfAntiforgeryPolicyExists() + { + // Arrange + var expected = Mock.Of(); + + var descriptor = new PageActionDescriptor(); + var provider = new AutoValidateAntiforgeryPageApplicationModelProvider(); + var context = new PageApplicationModelProviderContext(descriptor, typeof(object).GetTypeInfo()) + { + PageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()) + { + Filters = { expected }, + }, + }; + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + actual => Assert.Same(expected, actual)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs deleted file mode 100644 index 20d81e5c69..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AutoValidateAntiforgeryPageApplicationModelProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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 System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal -{ - public class AutoValidateAntiforgeryPageApplicationModelProviderTest - { - [Fact] - public void OnProvidersExecuting_AddsFiltersToModel() - { - // Arrange - var actionDescriptor = new PageActionDescriptor(); - var applicationModel = new PageApplicationModel( - actionDescriptor, - typeof(object).GetTypeInfo(), - new object[0]); - var applicationModelProvider = new AutoValidateAntiforgeryPageApplicationModelProvider(); - var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeof(object).GetTypeInfo()) - { - PageApplicationModel = applicationModel, - }; - - // Act - applicationModelProvider.OnProvidersExecuting(context); - - // Assert - Assert.Collection( - applicationModel.Filters, - filter => Assert.IsType(filter)); - } - } -} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml new file mode 100644 index 0000000000..9e36e83e2a --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml @@ -0,0 +1,5 @@ +@page +@model AntiforgeryDefaultModel +
+ +
diff --git a/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml.cs b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml.cs new file mode 100644 index 0000000000..b413f25aa5 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/AntiforgeryDefault.cshtml.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.RazorPages; + +namespace RazorPagesWebSite +{ + public class AntiforgeryDefaultModel : PageModel + { + public void OnGet() + { + } + + public void OnPost() + { + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml new file mode 100644 index 0000000000..336e79a803 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml @@ -0,0 +1,5 @@ +@page +@model IgnoreAntiforgeryModel +
+ +
diff --git a/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml.cs b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml.cs new file mode 100644 index 0000000000..3ee65db81d --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/IgnoreAntiforgery.cshtml.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + [IgnoreAntiforgeryToken] + public class IgnoreAntiforgeryModel : PageModel + { + public void OnGet() + { + } + + public void OnPost() + { + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/_ViewImports.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/_ViewImports.cshtml new file mode 100644 index 0000000000..aaf882de29 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Antiforgery/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"