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.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs
index ad8187cb78..23384fa9f7 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs
@@ -57,11 +57,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
foreach (var item in _razorFileSystem.EnumerateItems(_pagesOptions.RootDirectory))
{
- if (!IsRouteable(item))
- {
- continue;
- }
-
var relativePath = item.CombinedPath;
if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
{
@@ -99,11 +94,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
foreach (var item in _razorFileSystem.EnumerateItems(AreaRootDirectory))
{
- if (!IsRouteable(item))
- {
- continue;
- }
-
var relativePath = item.CombinedPath;
if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
{
@@ -125,11 +115,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
}
}
-
- private static bool IsRouteable(RazorProjectItem item)
- {
- // Pages like _ViewImports should not be routeable.
- return !item.FileName.StartsWith("_", StringComparison.OrdinalIgnoreCase);
- }
}
}
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.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs
index 1e2ced5c55..0ddf9f3af4 100644
--- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
-using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Hosting;
@@ -563,6 +562,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
}
+ [Fact]
+ public void OnProvidersExecuting_AllowsRazorFilesWithUnderscorePrefix()
+ {
+ // Arrange
+ var descriptors = new[]
+ {
+ CreateVersion_2_1_Descriptor("/Pages/_About.cshtml"),
+ CreateVersion_2_1_Descriptor("/Pages/Home.cshtml"),
+ };
+
+ var provider = CreateProvider(descriptors: descriptors);
+ var context = new PageRouteModelProviderContext();
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ Assert.Collection(
+ context.RouteModels,
+ result =>
+ {
+ Assert.Equal("/Pages/_About.cshtml", result.RelativePath);
+ Assert.Equal("/_About", result.ViewEnginePath);
+ Assert.Collection(
+ result.Selectors,
+ selector => Assert.Equal("_About", selector.AttributeRouteModel.Template));
+ Assert.Collection(
+ result.RouteValues.OrderBy(k => k.Key),
+ kvp =>
+ {
+ Assert.Equal("page", kvp.Key);
+ Assert.Equal("/_About", kvp.Value);
+ });
+ },
+ result =>
+ {
+ Assert.Equal("/Pages/Home.cshtml", result.RelativePath);
+ Assert.Equal("/Home", result.ViewEnginePath);
+ Assert.Collection(
+ result.Selectors,
+ selector => Assert.Equal("Home", selector.AttributeRouteModel.Template));
+ Assert.Collection(
+ result.RouteValues.OrderBy(k => k.Key),
+ kvp =>
+ {
+ Assert.Equal("page", kvp.Key);
+ Assert.Equal("/Home", kvp.Value);
+ });
+ });
+ }
+
[Fact]
public void GetRouteTemplate_ReturnsPathFromRazorPageAttribute()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs
index e6bc51da23..4b8aa8e581 100644
--- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs
@@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
fileSystem.Add(new TestRazorProjectItem("/Areas/Products/Pages/Manage/Categories.cshtml", "@page"));
fileSystem.Add(new TestRazorProjectItem("/Areas/Products/Pages/Index.cshtml", "@page"));
fileSystem.Add(new TestRazorProjectItem("/Areas/Products/Pages/List.cshtml", "@page \"{sortOrder?}\""));
- fileSystem.Add(new TestRazorProjectItem("/Areas/Products/Pages/_ViewStart.cshtml", "@page"));
+ fileSystem.Add(new TestRazorProjectItem("/Areas/Products/Pages/_Test.cshtml", "@page"));
var optionsManager = Options.Create(new RazorPagesOptions { AllowAreas = true });
var provider = new RazorProjectPageRouteModelProvider(fileSystem, optionsManager, NullLoggerFactory.Instance);
@@ -102,6 +102,24 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
},
model =>
+ {
+ Assert.Equal("/Areas/Products/Pages/_Test.cshtml", model.RelativePath);
+ Assert.Equal("/_Test", model.ViewEnginePath);
+ Assert.Collection(model.Selectors,
+ selector => Assert.Equal("Products/_Test", selector.AttributeRouteModel.Template));
+ Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
+ kvp =>
+ {
+ Assert.Equal("area", kvp.Key);
+ Assert.Equal("Products", kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("page", kvp.Key);
+ Assert.Equal("/_Test", kvp.Value);
+ });
+ },
+ model =>
{
Assert.Equal("/Areas/Products/Pages/Manage/Categories.cshtml", model.RelativePath);
Assert.Equal("/Manage/Categories", model.ViewEnginePath);
@@ -272,37 +290,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
});
}
- [Fact]
- public void OnProvidersExecuting_SkipsPagesStartingWithUnderscore()
- {
- // Arrange
- var fileSystem = new VirtualRazorProjectFileSystem();
- fileSystem.Add(new TestRazorProjectItem("/Pages/Home.cshtml", "@page"));
- fileSystem.Add(new TestRazorProjectItem("/Pages/_Layout.cshtml", "@page"));
-
- var optionsManager = Options.Create(new RazorPagesOptions());
- optionsManager.Value.RootDirectory = "/";
- var provider = new RazorProjectPageRouteModelProvider(fileSystem, optionsManager, NullLoggerFactory.Instance);
- var context = new PageRouteModelProviderContext();
-
- // Act
- provider.OnProvidersExecuting(context);
-
- // Assert
- Assert.Collection(context.RouteModels,
- model =>
- {
- Assert.Equal("/Pages/Home.cshtml", model.RelativePath);
- });
- }
-
[Fact]
public void OnProvidersExecuting_DiscoversFilesUnderBasePath()
{
// Arrange
var fileSystem = new VirtualRazorProjectFileSystem();
fileSystem.Add(new TestRazorProjectItem("/Pages/Index.cshtml", "@page"));
- fileSystem.Add(new TestRazorProjectItem("/Pages/_Layout.cshtml", "@page"));
+ fileSystem.Add(new TestRazorProjectItem("/Pages/_Layout.cshtml", ""));
fileSystem.Add(new TestRazorProjectItem("/NotPages/Index.cshtml", "@page"));
fileSystem.Add(new TestRazorProjectItem("/NotPages/_Layout.cshtml", "@page"));
fileSystem.Add(new TestRazorProjectItem("/Index.cshtml", "@page"));
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)
+ { }
+ }
+}