Allow IgnoreAntiForgeryToken applied on Razor Page models to work

Fixes #7795
This commit is contained in:
Pranav K 2018-06-12 12:13:59 -07:00
parent 6c2ef122f8
commit 287a3c5e69
12 changed files with 269 additions and 84 deletions

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,5 @@
@page
@model AntiforgeryDefaultModel
<form method="post">
</form>

View File

@ -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
{
}
}

View File

@ -0,0 +1,5 @@
@page
@model IgnoreAntiforgeryModel
<form method="post">
</form>

View File

@ -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
{
}
}

View File

@ -0,0 +1 @@
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"