Skip to content

Make AuthorizeFilter work in endpoint routing #9099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 11 additions & 26 deletions src/Components/test/testassets/TestServer/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using BasicTestApp;
using BasicTestApp.RouterTest;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -38,7 +37,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseDeveloperExceptionPage();
}

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

app.UseRouting();

Expand Down Expand Up @@ -82,29 +91,5 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
});
}

private static void AllowCorsForAnyLocalhostPort(IApplicationBuilder app)
{
// It's not enough just to return "Access-Control-Allow-Origin: *", because
// browsers don't allow wildcards in conjunction with credentials. So we must
// specify explicitly which origin we want to allow.
app.Use((context, next) =>
{
if (context.Request.Headers.TryGetValue("origin", out var incomingOriginValue))
{
var origin = incomingOriginValue.ToArray()[0];
if (origin.StartsWith("http://localhost:") || origin.StartsWith("http://127.0.0.1:"))
{
context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
context.Response.Headers.Add("Access-Control-Allow-Credentials", "true");
context.Response.Headers.Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,DELETE,OPTIONS");
context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,TestHeader,another-header");
context.Response.Headers.Add("Access-Control-Expose-Headers", "MyCustomHeader,TestHeader,another-header");
}
}

return next();
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 Identity.DefaultUI.WebSite;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests
{
public class IdentityUserAuthorizationWithoutEndpointRoutingTests : AuthorizationTests<StartupWithoutEndpointRouting, IdentityDbContext>
{
public IdentityUserAuthorizationWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory)
: base(serverFactory)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 Identity.DefaultUI.WebSite;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests
{
public class IdentityUserLoginWithoutEndpointRoutingTests : LoginTests<StartupWithoutEndpointRouting, IdentityDbContext>
{
public IdentityUserLoginWithoutEndpointRoutingTests(ServerFactory<StartupWithoutEndpointRouting, IdentityDbContext> serverFactory) : base(serverFactory)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public virtual void ConfigureServices(IServiceCollection services)
}

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

app.UseAuthentication();

// This has to be disabled due to https://github.com/aspnet/AspNetCore/issues/8387
//
// UseAuthorization does not currently work with Razor pages, and it impacts
// many of the tests here. Uncomment when this is fixed so that we test what is recommended
// for users.
//
//app.UseAuthorization();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;

namespace Identity.DefaultUI.WebSite
{
public class StartupWithoutEndpointRouting : StartupBase<IdentityUser, IdentityDbContext>
{
public StartupWithoutEndpointRouting(IConfiguration configuration) : base(configuration)
{
}

public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
services.AddMvc(options => options.EnableEndpointRouting = false);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// This prevents running out of file watchers on some linux machines
((PhysicalFileProvider)env.WebRootFileProvider).UseActivePolling = false;

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseAuthentication();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseMvc();
}
}
}
15 changes: 15 additions & 0 deletions src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,23 @@ private async Task InvokeCore(HttpContext context, ICorsPolicyProvider corsPolic
// Get the most significant CORS metadata for the endpoint
// For backwards compatibility reasons this is then downcast to Enable/Disable metadata
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();

if (corsMetadata is IDisableCorsAttribute)
{
var isOptionsRequest = string.Equals(
context.Request.Method,
CorsConstants.PreflightHttpMethod,
StringComparison.OrdinalIgnoreCase);

var isCorsPreflightRequest = isOptionsRequest && context.Request.Headers.ContainsKey(CorsConstants.AccessControlRequestMethod);

if (isCorsPreflightRequest)
{
// If this is a preflight request, and we disallow CORS, complete the request
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}

await _next(context);
return;
}
Expand Down
62 changes: 61 additions & 1 deletion src/Middleware/CORS/test/UnitTests/CorsMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Microsoft.AspNetCore.Http.Endpoints;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
Expand Down Expand Up @@ -626,6 +625,67 @@ public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicyName_R
Times.Once);
}

[Fact]
public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ReturnsNoContentForPreflightRequest()
{
// Arrange
var corsService = Mock.Of<ICorsService>();
var policyProvider = Mock.Of<ICorsPolicyProvider>();
var loggerFactory = NullLoggerFactory.Instance;

var middleware = new CorsMiddleware(
c => { throw new Exception("Should not be called."); },
corsService,
loggerFactory,
"DefaultPolicyName");

var httpContext = new DefaultHttpContext();
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint"));
httpContext.Request.Method = "OPTIONS";
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" });

// Act
await middleware.Invoke(httpContext, policyProvider);

// Assert
Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode);
}

[Fact]
public async Task Invoke_HasEndpointWithEnableMetadata_HasSignificantDisableCors_ExecutesNextMiddleware()
{
// Arrange
var executed = false;
var corsService = Mock.Of<ICorsService>();
var policyProvider = Mock.Of<ICorsPolicyProvider>();
var loggerFactory = NullLoggerFactory.Instance;

var middleware = new CorsMiddleware(
c =>
{
executed = true;
return Task.CompletedTask;
},
corsService,
loggerFactory,
"DefaultPolicyName");

var httpContext = new DefaultHttpContext();
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute(), new DisableCorsAttribute()), "Test endpoint"));
httpContext.Request.Method = "GET";
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { "GET" });

// Act
await middleware.Invoke(httpContext, policyProvider);

// Assert
Assert.True(executed);
Mock.Get(policyProvider).Verify(v => v.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()), Times.Never());
Mock.Get(corsService).Verify(v => v.EvaluatePolicy(It.IsAny<HttpContext>(), It.IsAny<CorsPolicy>()), Times.Never());
}

[Fact]
public async Task Invoke_HasEndpointWithEnableMetadata_MiddlewareHasPolicy_RunsCorsWithPolicyName()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,16 @@ private static void AddActionConstraints(SelectorModel selector, IList<IActionCo
}
}

private static void AddEndpointMetadata(SelectorModel selector, IList<object> metadata)
private static void AddEndpointMetadata(SelectorModel selector, IList<object> controllerMetadata)
{
if (metadata != null)
if (controllerMetadata != null)
{
for (var i = 0; i < metadata.Count; i++)
// It is criticial to get the order in which metadata appears in endpoint metadata correct. More significant metadata
// must appear later in the sequence. In this case, the values in `controllerMetadata` should have their order
// preserved, but appear earlier than the entries in `selector.EndpointMetadata`.
for (var i = 0; i < controllerMetadata.Count; i++)
{
selector.EndpointMetadata.Add(metadata[i]);
selector.EndpointMetadata.Insert(i, controllerMetadata[i]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
internal class AuthorizationApplicationModelProvider : IApplicationModelProvider
{
private readonly MvcOptions _mvcOptions;
private readonly IAuthorizationPolicyProvider _policyProvider;

public AuthorizationApplicationModelProvider(IAuthorizationPolicyProvider policyProvider)
public AuthorizationApplicationModelProvider(
IAuthorizationPolicyProvider policyProvider,
IOptions<MvcOptions> mvcOptions)
{
_policyProvider = policyProvider;
_mvcOptions = mvcOptions.Value;
}

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

if (_mvcOptions.EnableEndpointRouting)
{
// When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform.
// Consequently we do not need to convert authorization attributes to filters.
return;
}

foreach (var controllerModel in context.Result.Controllers)
{
var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray();
Expand Down
3 changes: 3 additions & 0 deletions src/Mvc/Mvc.Core/src/ApplicationModels/SelectorModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public SelectorModel(SelectorModel other)

public IList<IActionConstraintMetadata> ActionConstraints { get; }

/// <summary>
/// Gets the <see cref="EndpointMetadata"/> associated with the <see cref="SelectorModel"/>.
/// </summary>
public IList<object> EndpointMetadata { get; }
}
}
Loading