diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs index 0ababdcddc..5af62882b7 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs @@ -4,6 +4,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { // This is used to store the uncombined parts of the final page route + // Note: This type name is referenced by name in AuthorizationMiddleware, do not change this without addressing https://github.com/aspnet/AspNetCore/issues/7011 internal class PageRouteMetadata { public PageRouteMetadata(string pageRoute, string routeTemplate) diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index b36fe5af37..56e8785254 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1190,11 +1190,11 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa public async Task AuthFiltersAppliedToPageModel_AreExecuted() { // Act - var response = await Client.GetAsync("/ModelWithAuthFilter"); + var response = await Client.GetAsync("/Pages/ModelWithAuthFilter"); // Assert Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); - Assert.Equal("/Login?ReturnUrl=%2FModelWithAuthFilter", response.Headers.Location.PathAndQuery); + Assert.Equal("/Login?ReturnUrl=%2FPages%2FModelWithAuthFilter", response.Headers.Location.PathAndQuery); } [Fact] diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs new file mode 100644 index 0000000000..6e7be902ff --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithEndpointRoutingTest.cs @@ -0,0 +1,66 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class RazorPagesWithEndpointRoutingTest : IClassFixture> + { + public RazorPagesWithEndpointRoutingTest(MvcTestFixture fixture) + { + Client = fixture.CreateDefaultClient(); + } + + public HttpClient Client { get; } + + [Fact] + public async Task Authorize_AppliedUsingConvention_Works() + { + // Act + var response = await Client.GetAsync("/Conventions/AuthFolder"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.Redirect); + Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuthFolder", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task Authorize_AppliedUsingConvention_CanByOverridenByAllowAnonymousAppliedToModel() + { + // Act + var response = await Client.GetAsync("/Conventions/AuthFolder/AnonymousViaModel"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from Anonymous", content.Trim()); + } + + [Fact] + public async Task Authorize_AppliedUsingAttributeOnModel_Works() + { + // Act + var response = await Client.GetAsync("/ModelWithAuthFilter"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.Redirect); + Assert.Equal("/Login?ReturnUrl=%2FModelWithAuthFilter", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task Authorize_WithEndpointRouting_WorksForControllers() + { + // Act + var response = await Client.GetAsync("/AuthorizedAction/Index"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.Redirect); + Assert.Equal("/Login?ReturnUrl=%2FAuthorizedAction%2FIndex", response.Headers.Location.PathAndQuery); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Controllers/AuthorizedActionController.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Controllers/AuthorizedActionController.cs new file mode 100644 index 0000000000..5967c595ec --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Controllers/AuthorizedActionController.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.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RazorPagesWebSite.Controllers +{ + [Route("[controller]/[action]")] + [Authorize] + public class AuthorizedActionController : Controller + { + public IActionResult Index() => Ok(); + } +} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/ModelWithAuthFilter.cs similarity index 100% rename from src/Mvc/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs rename to src/Mvc/test/WebSites/RazorPagesWebSite/Pages/ModelWithAuthFilter.cs diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/ModelWithAuthFilter.cshtml similarity index 100% rename from src/Mvc/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml rename to src/Mvc/test/WebSites/RazorPagesWebSite/Pages/ModelWithAuthFilter.cshtml diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs new file mode 100644 index 0000000000..3483ad0b0f --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithEndpointRouting.cs @@ -0,0 +1,36 @@ +// 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.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace RazorPagesWebSite +{ + public class StartupWithEndpointRouting + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => options.LoginPath = "/Login"); + + services.AddMvc() + .AddRazorPagesOptions(options => + { + options.Conventions.AuthorizeFolder("/Admin"); + }) + .SetCompatibilityVersion(CompatibilityVersion.Latest); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(routes => + { + routes.MapApplication(); + }); + + app.UseAuthorization(); + } + } +} diff --git a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs index e33bdd9bce..5b6426dc5e 100644 --- a/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs +++ b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Endpoints; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Authorization @@ -45,11 +44,19 @@ namespace Microsoft.AspNetCore.Authorization throw new ArgumentNullException(nameof(context)); } + var endpoint = context.GetEndpoint(); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/7011. Do not use the AuthorizationMiddleware for Razor Pages + if (endpoint.Metadata.Any(m => m.GetType().FullName == "Microsoft.AspNetCore.Mvc.ApplicationModels.PageRouteMetadata")) + { + await _next(context); + return; + } + // Flag to indicate to other systems, e.g. MVC, that authorization middleware was run for this request context.Items[AuthorizationMiddlewareInvokedKey] = AuthorizationMiddlewareInvokedValue; // IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter - var endpoint = context.GetEndpoint(); var authorizeData = endpoint?.Metadata.GetOrderedMetadata() ?? Array.Empty(); var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData); if (policy == null)