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);
|
var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext);
|
||||||
|
|
||||||
// Allow Anonymous skips all authorization
|
// Allow Anonymous skips all authorization
|
||||||
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
|
if (HasAllowAnonymous(context.Filters))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -218,5 +218,18 @@ namespace Microsoft.AspNetCore.Mvc.Authorization
|
||||||
var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
|
var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
|
||||||
return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Xml.Linq;
|
using AngleSharp.Dom.Html;
|
||||||
|
using AngleSharp.Parser.Html;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
public static class AntiforgeryTestHelper
|
public static class AntiforgeryTestHelper
|
||||||
{
|
{
|
||||||
|
public static string RetrieveAntiforgeryToken(string htmlContent)
|
||||||
|
=> RetrieveAntiforgeryToken(htmlContent, actionUrl: string.Empty);
|
||||||
|
|
||||||
public static string RetrieveAntiforgeryToken(string htmlContent, string actionUrl)
|
public static string RetrieveAntiforgeryToken(string htmlContent, string actionUrl)
|
||||||
{
|
{
|
||||||
htmlContent = "<Root>" + htmlContent + "</Root>";
|
var parser = new HtmlParser();
|
||||||
var reader = new StringReader(htmlContent);
|
var htmlDocument = parser.Parse(htmlContent);
|
||||||
var htmlDocument = XDocument.Load(reader);
|
|
||||||
|
|
||||||
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 &&
|
continue;
|
||||||
input.Attribute("type") != null &&
|
}
|
||||||
input.Attribute("type").Value == "hidden" &&
|
|
||||||
(input.Attribute("name").Value == "__RequestVerificationToken" ||
|
var name = input.GetAttribute("name");
|
||||||
input.Attribute("name").Value == "HtmlEncode[[__RequestVerificationToken]]"))
|
if (name == "__RequestVerificationToken" || name == "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)
|
public static CookieMetadata RetrieveAntiforgeryCookie(HttpResponseMessage response)
|
||||||
|
|
|
||||||
|
|
@ -552,5 +552,72 @@ Hello from /Pages/Shared/";
|
||||||
var title = document.QuerySelector("title").TextContent;
|
var title = document.QuerySelector("title").TextContent;
|
||||||
Assert.Equal("View Data in Pages", title);
|
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