Skip to content

Commit b93bc43

Browse files
authored
Make AuthorizeFilter work in endpoint routing (#9099)
* Make AuthorizeFilter work in endpoint routing Fixes #8387
1 parent 67e0872 commit b93bc43

File tree

69 files changed

+1892
-296
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1892
-296
lines changed

src/Components/test/testassets/TestServer/Startup.cs

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using BasicTestApp;
2-
using BasicTestApp.RouterTest;
32
using Microsoft.AspNetCore.Builder;
43
using Microsoft.AspNetCore.Components.Server;
54
using Microsoft.AspNetCore.Hosting;
@@ -38,7 +37,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
3837
app.UseDeveloperExceptionPage();
3938
}
4039

41-
AllowCorsForAnyLocalhostPort(app);
40+
// It's not enough just to return "Access-Control-Allow-Origin: *", because
41+
// browsers don't allow wildcards in conjunction with credentials. So we must
42+
// specify explicitly which origin we want to allow.
43+
app.UseCors(policy =>
44+
{
45+
policy.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
46+
.AllowAnyHeader()
47+
.WithExposedHeaders("MyCustomHeader")
48+
.AllowAnyMethod()
49+
.AllowCredentials();
50+
});
4251

4352
app.UseRouting();
4453

@@ -82,29 +91,5 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
8291
});
8392
});
8493
}
85-
86-
private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app)
87-
{
88-
// It's not enough just to return "Access-Control-Allow-Origin: *", because
89-
// browsers don't allow wildcards in conjunction with credentials. So we must
90-
// specify explicitly which origin we want to allow.
91-
app.Use((context, next) =>
92-
{
93-
if (context.Request.Headers.TryGetValue("origin", out var incomingOriginValue))
94-
{
95-
var origin = incomingOriginValue.ToArray()[0];
96-
if (origin.StartsWith("http://localhost:") || origin.StartsWith("http://127.0.0.1:"))
97-
{
98-
context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
99-
context.Response.Headers.Add("Access-Control-Allow-Credentials", "true");
100-
context.Response.Headers.Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,DELETE,OPTIONS");
101-
context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,TestHeader,another-header");
102-
context.Response.Headers.Add("Access-Control-Expose-Headers", "MyCustomHeader,TestHeader,another-header");
103-
}
104-
}
105-
106-
return next();
107-
});
108-
}
10994
}
11095
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Identity.DefaultUI.WebSite;
5+
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
6+
7+
namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests
8+
{
9+
public class IdentityUserAuthorizationWithoutEndpointRoutingTests : AuthorizationTests<StartupWithoutEndpointRouting, IdentityDbContext>
10+
{
11+
public IdentityUserAuthorizationWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory)
12+
: base(serverFactory)
13+
{
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Identity.DefaultUI.WebSite;
5+
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
6+
using Xunit.Abstractions;
7+
8+
namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests
9+
{
10+
public class IdentityUserLoginWithoutEndpointRoutingTests : LoginTests<StartupWithoutEndpointRouting, IdentityDbContext>
11+
{
12+
public IdentityUserLoginWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory) : base(serverFactory)
13+
{
14+
}
15+
}
16+
}

src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public virtual void ConfigureServices(IServiceCollection services)
5757
}
5858

5959
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
60-
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
60+
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
6161
{
6262
// This prevents running out of file watchers on some linux machines
6363
((PhysicalFileProvider)env.WebRootFileProvider).UseActivePolling = false;
@@ -80,14 +80,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
8080
app.UseRouting();
8181

8282
app.UseAuthentication();
83-
84-
// This has to be disabled due to https://github.com/aspnet/AspNetCore/issues/8387
85-
//
86-
// UseAuthorization does not currently work with Razor pages, and it impacts
87-
// many of the tests here. Uncomment when this is fixed so that we test what is recommended
88-
// for users.
89-
//
90-
//app.UseAuthorization();
83+
app.UseAuthorization();
9184

9285
app.UseEndpoints(endpoints =>
9386
{
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.AspNetCore.Identity;
7+
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.FileProviders;
11+
using Microsoft.Extensions.Hosting;
12+
13+
namespace Identity.DefaultUI.WebSite
14+
{
15+
public class StartupWithoutEndpointRouting : StartupBase<IdentityUser, IdentityDbContext>
16+
{
17+
public StartupWithoutEndpointRouting(IConfiguration configuration) : base(configuration)
18+
{
19+
}
20+
21+
public override void ConfigureServices(IServiceCollection services)
22+
{
23+
base.ConfigureServices(services);
24+
services.AddMvc(options => options.EnableEndpointRouting = false);
25+
}
26+
27+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
28+
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
29+
{
30+
// This prevents running out of file watchers on some linux machines
31+
((PhysicalFileProvider)env.WebRootFileProvider).UseActivePolling = false;
32+
33+
if (env.IsDevelopment())
34+
{
35+
app.UseDeveloperExceptionPage();
36+
app.UseDatabaseErrorPage();
37+
}
38+
else
39+
{
40+
app.UseExceptionHandler("/Error");
41+
app.UseHsts();
42+
}
43+
44+
app.UseAuthentication();
45+
46+
app.UseHttpsRedirection();
47+
app.UseStaticFiles();
48+
app.UseCookiePolicy();
49+
50+
app.UseMvc();
51+
}
52+
}
53+
}

src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,23 @@ private async Task InvokeCore(HttpContext context, ICorsPolicyProvider corsPolic
145145
// Get the most significant CORS metadata for the endpoint
146146
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
147147
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();
148+
148149
if (corsMetadata is IDisableCorsAttribute)
149150
{
151+
var isOptionsRequest = string.Equals(
152+
context.Request.Method,
153+
CorsConstants.PreflightHttpMethod,
154+
StringComparison.OrdinalIgnoreCase);
155+
156+
var isCorsPreflightRequest = isOptionsRequest && context.Request.Headers.ContainsKey(CorsConstants.AccessControlRequestMethod);
157+
158+
if (isCorsPreflightRequest)
159+
{
160+
// If this is a preflight request, and we disallow CORS, complete the request
161+
context.Response.StatusCode = StatusCodes.Status204NoContent;
162+
return;
163+
}
164+
150165
await _next(context);
151166
return;
152167
}

src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.AspNetCore.Http.Endpoints;
1212
using Microsoft.AspNetCore.TestHost;
1313
using Microsoft.Extensions.DependencyInjection;
14-
using Microsoft.Extensions.Logging;
1514
using Microsoft.Extensions.Logging.Abstractions;
1615
using Moq;
1716
using Xunit;
@@ -626,6 +625,67 @@ public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicyName_R
626625
Times.Once);
627626
}
628627

628+
[Fact]
629+
public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ReturnsNoContentForPreflightRequest()
630+
{
631+
// Arrange
632+
var corsService = Mock.Of<ICorsService>();
633+
var policyProvider = Mock.Of<ICorsPolicyProvider>();
634+
var loggerFactory = NullLoggerFactory.Instance;
635+
636+
var middleware = new CorsMiddleware(
637+
c => { throw new Exception("Should not be called."); },
638+
corsService,
639+
loggerFactory,
640+
"DefaultPolicyName");
641+
642+
var httpContext = new DefaultHttpContext();
643+
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint"));
644+
httpContext.Request.Method = "OPTIONS";
645+
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
646+
httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" });
647+
648+
// Act
649+
await middleware.Invoke(httpContext, policyProvider);
650+
651+
// Assert
652+
Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode);
653+
}
654+
655+
[Fact]
656+
public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ExecutesNextMiddleware()
657+
{
658+
// Arrange
659+
var executed = false;
660+
var corsService = Mock.Of<ICorsService>();
661+
var policyProvider = Mock.Of<ICorsPolicyProvider>();
662+
var loggerFactory = NullLoggerFactory.Instance;
663+
664+
var middleware = new CorsMiddleware(
665+
c =>
666+
{
667+
executed = true;
668+
return Task.CompletedTask;
669+
},
670+
corsService,
671+
loggerFactory,
672+
"DefaultPolicyName");
673+
674+
var httpContext = new DefaultHttpContext();
675+
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint"));
676+
httpContext.Request.Method = "GET";
677+
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
678+
httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" });
679+
680+
// Act
681+
await middleware.Invoke(httpContext, policyProvider);
682+
683+
// Assert
684+
Assert.True(executed);
685+
Mock.Get(policyProvider).Verify(v => v.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()), Times.Never());
686+
Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny<HttpContext>(), It.IsAny<CorsPolicy>()), Times.Never());
687+
}
688+
629689
[Fact]
630690
public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName()
631691
{

src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,16 @@ private static void AddActionConstraints(SelectorModel selector, IList<IActionCo
127127
}
128128
}
129129

130-
private static void AddEndpointMetadata(SelectorModel selector, IList<object> metadata)
130+
private static void AddEndpointMetadata(SelectorModel selector, IList<object> controllerMetadata)
131131
{
132-
if (metadata != null)
132+
if (controllerMetadata != null)
133133
{
134-
for (var i = 0; i < metadata.Count; i++)
134+
// It is criticial to get the order in which metadata appears in endpoint metadata correct. More significant metadata
135+
// must appear later in the sequence. In this case, the values in `controllerMetadata` should have their order
136+
// preserved, but appear earlier than the entries in `selector.EndpointMetadata`.
137+
for (var i = 0; i < controllerMetadata.Count; i++)
135138
{
136-
selector.EndpointMetadata.Add(metadata[i]);
139+
selector.EndpointMetadata.Insert(i, controllerMetadata[i]);
137140
}
138141
}
139142
}

src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66
using System.Linq;
77
using Microsoft.AspNetCore.Authorization;
88
using Microsoft.AspNetCore.Mvc.Authorization;
9+
using Microsoft.Extensions.Options;
910

1011
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
1112
{
1213
internal class AuthorizationApplicationModelProvider : IApplicationModelProvider
1314
{
15+
private readonly MvcOptions _mvcOptions;
1416
private readonly IAuthorizationPolicyProvider _policyProvider;
1517

16-
public AuthorizationApplicationModelProvider(IAuthorizationPolicyProvider policyProvider)
18+
public AuthorizationApplicationModelProvider(
19+
IAuthorizationPolicyProvider policyProvider,
20+
IOptions<MvcOptions> mvcOptions)
1721
{
1822
_policyProvider = policyProvider;
23+
_mvcOptions = mvcOptions.Value;
1924
}
2025

2126
public int Order => -1000 + 10;
@@ -32,6 +37,13 @@ public void OnProvidersExecuting(ApplicationModelProviderContext context)
3237
throw new ArgumentNullException(nameof(context));
3338
}
3439

40+
if (_mvcOptions.EnableEndpointRouting)
41+
{
42+
// When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform.
43+
// Consequently we do not need to convert authorization attributes to filters.
44+
return;
45+
}
46+
3547
foreach (var controllerModel in context.Result.Controllers)
3648
{
3749
var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray();

src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public SelectorModel(SelectorModel other)
3535

3636
public IList<IActionConstraintMetadata> ActionConstraints { get; }
3737

38+
/// <summary>
39+
/// Gets the <see cref="EndpointMetadata"/> associated with the <see cref="SelectorModel"/>.
40+
/// </summary>
3841
public IList<object> EndpointMetadata { get; }
3942
}
4043
}

0 commit comments

Comments
 (0)