Allow IgnoreAntiForgeryToken applied on Razor Page models to work
Fixes #7795
This commit is contained in:
parent
6c2ef122f8
commit
287a3c5e69
|
|
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Mvc.Authorization
|
|||
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 @@ namespace Microsoft.AspNetCore.Mvc.Authorization
|
|||
var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
|
||||
return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
|
||||
}
|
||||
|
||||
private static bool HasAllowAnonymous(IList<IFilterMetadata> filters)
|
||||
{
|
||||
for (var i = 0; i < filters.Count; i++)
|
||||
{
|
||||
if (filters[i] is IAllowAnonymousFilter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IAntiforgeryPolicy>().Any())
|
||||
{
|
||||
// Always require an antiforgery token on post
|
||||
pageApplicationModel.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "<Root>" + htmlContent + "</Root>";
|
||||
var reader = new StringReader(htmlContent);
|
||||
var htmlDocument = XDocument.Load(reader);
|
||||
var parser = new HtmlParser();
|
||||
var htmlDocument = parser.Parse(htmlContent);
|
||||
|
||||
foreach (var form in htmlDocument.Descendants("form"))
|
||||
return RetrieveAntiforgeryToken(htmlDocument);
|
||||
}
|
||||
|
||||
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"))
|
||||
{
|
||||
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;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = input.GetAttribute("name");
|
||||
if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]")
|
||||
{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -552,5 +552,72 @@ Hello from /Pages/Shared/";
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AutoValidateAntiforgeryTokenAttribute>(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<object>())
|
||||
{
|
||||
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<IAntiforgeryPolicy>();
|
||||
|
||||
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<object>())
|
||||
{
|
||||
Filters = { expected },
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
provider.OnProvidersExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
context.PageApplicationModel.Filters,
|
||||
actual => Assert.Same(expected, actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AutoValidateAntiforgeryTokenAttribute>(filter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@page
|
||||
@model AntiforgeryDefaultModel
|
||||
<form method="post">
|
||||
|
||||
</form>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// 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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@page
|
||||
@model IgnoreAntiforgeryModel
|
||||
<form method="post">
|
||||
|
||||
</form>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
|
||||
Loading…
Reference in New Issue