Throw when UseAuthorization is incorrectly configured

* Update AuthZ & Cors middlewares to only set endpoint routing metadata when
  executing in the context of endpoint routing
* Add analyzers for incorrect UseAuth use

Fixes https://github.com/aspnet/AspNetCore/issues/14049
This commit is contained in:
Pranav K 2019-09-24 15:08:51 -07:00 committed by Artak
parent 51ae61baca
commit 29a1d50e02
23 changed files with 633 additions and 50 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Collections.Immutable;
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Analyzers
{
// ASP
BuildServiceProviderShouldNotCalledInConfigureServicesMethod,
IncorrectlyConfiguredAuthorizationMiddleware,
// MVC
UnsupportedUseMvcWithEndpointRouting,
@ -42,6 +43,15 @@ namespace Microsoft.AspNetCore.Analyzers
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/YJggeFn");
internal readonly static DiagnosticDescriptor IncorrectlyConfiguredAuthorizationMiddleware = new DiagnosticDescriptor(
"ASP0001",
"Authorization middleware is incorrectly configured.",
"The call to UseAuthorization should appear between app.UseRouting() and app.UseEndpoints(..) for authorization to be correctly evaluated.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/AA64fv1");
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -82,6 +82,7 @@ namespace Microsoft.AspNetCore.Analyzers
var analysis = builder.Build();
new UseMvcAnalyzer(analysis).AnalyzeSymbol(context);
new BuildServiceProviderValidator(analysis).AnalyzeSymbol(context);
new UseAuthorizationAnalyzer(analysis).AnalyzeSymbol(context);
});
}, SymbolKind.NamedType);

View File

@ -0,0 +1,82 @@
// 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.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Analyzers
{
internal class UseAuthorizationAnalyzer
{
private readonly StartupAnalysis _context;
public UseAuthorizationAnalyzer(StartupAnalysis context)
{
_context = context;
}
public void AnalyzeSymbol(SymbolAnalysisContext context)
{
Debug.Assert(context.Symbol.Kind == SymbolKind.NamedType);
Debug.Assert(StartupFacts.IsStartupClass(_context.StartupSymbols, (INamedTypeSymbol)context.Symbol));
var type = (INamedTypeSymbol)context.Symbol;
foreach (var middlewareAnalysis in _context.GetRelatedAnalyses<MiddlewareAnalysis>(type))
{
MiddlewareItem? useAuthorizationItem = default;
MiddlewareItem? useRoutingItem = default;
var length = middlewareAnalysis.Middleware.Length;
for (var i = length - 1; i >= 0; i-- )
{
var middlewareItem = middlewareAnalysis.Middleware[i];
var middleware = middlewareItem.UseMethod.Name;
if (middleware == "UseAuthorization")
{
if (useRoutingItem != null && useAuthorizationItem == null)
{
// This looks like
//
// app.UseAuthorization();
// ...
// app.UseRouting();
// app.UseEndpoints(...);
context.ReportDiagnostic(Diagnostic.Create(
StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware,
middlewareItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name));
}
useAuthorizationItem = middlewareItem;
}
else if (middleware == "UseEndpoints")
{
if (useAuthorizationItem != null)
{
// This configuration looks like
//
// app.UseRouting();
// app.UseEndpoints(...);
// ...
// app.UseAuthorization();
//
context.ReportDiagnostic(Diagnostic.Create(
StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware,
useAuthorizationItem.Operation.Syntax.GetLocation(),
middlewareItem.UseMethod.Name));
}
}
else if (middleware == "UseRouting")
{
useRoutingItem = middlewareItem;
}
}
}
}
}
}

View File

@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Analyzers
Assert.Collection(
middlewareAnalysis.Middleware,
item => Assert.Equal("UseAuthorization", item.UseMethod.Name),
item => Assert.Equal("UseStaticFiles", item.UseMethod.Name),
item => Assert.Equal("UseMiddleware", item.UseMethod.Name),
item => Assert.Equal("UseMvc", item.UseMethod.Name),
item => Assert.Equal("UseRouting", item.UseMethod.Name),
@ -228,5 +228,90 @@ namespace Microsoft.AspNetCore.Analyzers
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredCorrectly_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthConfiguredCorrectly));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationInvokedMultipleTimesInEndpointRoutingBlock_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthMultipleTimes));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredBeforeUseRouting_ReportsDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthBeforeUseRouting));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Collection(diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_UseAuthorizationConfiguredAfterUseEndpoints_ReportsDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthAfterUseEndpoints));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Collection(diagnostics,
diagnostic =>
{
Assert.Same(StartupAnalyzer.Diagnostics.IncorrectlyConfiguredAuthorizationMiddleware, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
});
}
[Fact]
public async Task StartupAnalyzer_MultipleUseAuthorization_ReportsNoDiagnostics()
{
// Arrange
var source = Read(nameof(TestFiles.StartupAnalyzerTest.UseAuthFallbackPolicy));
// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
// Assert
var middlewareAnalysis = Assert.Single(Analyses.OfType<MiddlewareAnalysis>());
Assert.NotEmpty(middlewareAnalysis.Middleware);
Assert.Empty(diagnostics);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
/*MM1*/app.UseMvcWithDefaultRoute();
app.UseAuthorization();
app.UseStaticFiles();
app.UseMiddleware<AuthorizationMiddleware>();
/*MM2*/app.UseMvc();

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
public void Configure(IApplicationBuilder app)
{
app.UseAuthorization();
app.UseStaticFiles();
app.UseMiddleware<AuthorizationMiddleware>();
/*MM*/app.UseMvc();

View File

@ -0,0 +1,17 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthAfterUseEndpoints
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(r => { });
/*MM*/app.UseAuthorization();
}
}
}

View File

@ -0,0 +1,18 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthBeforeUseRouting
{
public void Configure(IApplicationBuilder app)
{
app.UseFileServer();
/*MM*/app.UseAuthorization();
app.UseRouting();
app.UseEndpoints(r => { });
}
}
}

View File

@ -0,0 +1,17 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthConfiguredCorrectly
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(r => { });
}
}
}

View File

@ -0,0 +1,22 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthFallbackPolicy
{
public void Configure(IApplicationBuilder app)
{
// This sort of setup would be useful if the user wants to use Auth for non-endpoint content to be handled using the Fallback policy, while
// using the second instance for regular endpoint routing based auth. We do not want to produce a warning in this case.
app.UseAuthorization();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(r => { });
}
}
}

View File

@ -0,0 +1,18 @@
// 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;
namespace Microsoft.AspNetCore.Analyzers.TestFiles.StartupAnalyzerTest
{
public class UseAuthMultipleTimes
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthorization();
app.UseAuthorization();
app.UseEndpoints(r => { });
}
}
}

View File

@ -21,7 +21,16 @@ namespace TestServer
services.AddMvc();
services.AddCors(options =>
{
options.AddPolicy("AllowAll", _ => { /* Controlled below */ });
// 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.
options.AddPolicy("AllowAll", policy => policy
.SetIsOriginAllowed(host => host.StartsWith("http://localhost:") || host.StartsWith("http://127.0.0.1:"))
.AllowAnyHeader()
.WithExposedHeaders("MyCustomHeader")
.AllowAnyMethod()
.AllowCredentials());
});
}
@ -33,18 +42,6 @@ namespace TestServer
app.UseDeveloperExceptionPage();
}
// 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();
});
// Mount the server-side Blazor app on /subdir
app.Map("/subdir", app =>
{
@ -52,12 +49,14 @@ namespace TestServer
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
app.UseRouting();
app.UseCors();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
});
});
}
}

View File

@ -1,6 +1,7 @@
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -68,9 +69,7 @@ namespace TestServer
</html>");
var content = writer.ToString();
ctx.Response.ContentLength = content.Length;
using var responseWriter = new StreamWriter(ctx.Response.Body);
await responseWriter.WriteAsync(content);
await responseWriter.FlushAsync();
await ctx.Response.WriteAsync(content);
});
}
}

View File

@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "..\Security\Authorization\Policy\src\Microsoft.AspNetCore.Authorization.Policy.csproj", "{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors", "..\Middleware\CORS\src\Microsoft.AspNetCore.Cors.csproj", "{09168958-FD5B-4D25-8FBF-75E2C80D903B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -603,6 +607,30 @@ Global
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x64.Build.0 = Release|Any CPU
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.ActiveCfg = Release|Any CPU
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x64.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.ActiveCfg = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Debug|x86.Build.0 = Debug|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|Any CPU.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x64.Build.0 = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.ActiveCfg = Release|Any CPU
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B}.Release|x86.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x64.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.ActiveCfg = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Debug|x86.Build.0 = Debug|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|Any CPU.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x64.Build.0 = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.ActiveCfg = Release|Any CPU
{09168958-FD5B-4D25-8FBF-75E2C80D903B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -651,6 +679,8 @@ Global
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
{8BCAA9EC-0ACD-435C-BF8A-8C843499FF7B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
{09168958-FD5B-4D25-8FBF-75E2C80D903B} = {793FFE24-138A-4C3D-81AB-18D625E36230}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -14,8 +13,8 @@ namespace Microsoft.AspNetCore.Routing
{
internal sealed class EndpointMiddleware
{
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
private readonly ILogger _logger;
private readonly RequestDelegate _next;
@ -91,7 +90,7 @@ namespace Microsoft.AspNetCore.Routing
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
"but a middleware was not found that supports authorization." +
Environment.NewLine +
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.");
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
}
private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint)
@ -99,7 +98,7 @@ namespace Microsoft.AspNetCore.Routing
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " +
"but a middleware was not found that supports CORS." +
Environment.NewLine +
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.");
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).");
}
private static class Log

View File

@ -0,0 +1,240 @@
// 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.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Routing.FunctionalTests
{
public class EndpointRoutingIntegrationTest
{
private static readonly RequestDelegate TestDelegate = async context => await Task.Yield();
private static readonly string AuthErrorMessage = "Endpoint / contains authorization metadata, but a middleware was not found that supports authorization." +
Environment.NewLine +
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
private static readonly string CORSErrorMessage = "Endpoint / contains CORS metadata, but a middleware was not found that supports CORS." +
Environment.NewLine +
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
[Fact]
public async Task AuthorizationMiddleware_WhenNoAuthMetadataIsConfigured()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(b => b.Map("/", TestDelegate));
})
.ConfigureServices(services =>
{
services.AddAuthorization();
services.AddRouting();
});
using var server = new TestServer(builder);
var response = await server.CreateRequest("/").SendAsync("GET");
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task AuthorizationMiddleware_WhenEndpointIsNotFound()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(b => b.Map("/", TestDelegate));
})
.ConfigureServices(services =>
{
services.AddAuthorization();
services.AddRouting();
});
using var server = new TestServer(builder);
var response = await server.CreateRequest("/not-found").SendAsync("GET");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task AuthorizationMiddleware_WithAuthorizedEndpoint()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
})
.ConfigureServices(services =>
{
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
services.AddRouting();
});
using var server = new TestServer(builder);
var response = await server.CreateRequest("/").SendAsync("GET");
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task AuthorizationMiddleware_NotConfigured_Throws()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
})
.ConfigureServices(services =>
{
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
services.AddRouting();
});
using var server = new TestServer(builder);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
Assert.Equal(AuthErrorMessage, ex.Message);
}
[Fact]
public async Task AuthorizationMiddleware_NotConfigured_WhenEndpointIsNotFound()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
})
.ConfigureServices(services =>
{
services.AddRouting();
});
using var server = new TestServer(builder);
var response = await server.CreateRequest("/not-found").SendAsync("GET");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task AuthorizationMiddleware_ConfiguredBeforeRouting_Throws()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseAuthorization();
app.UseRouting();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
})
.ConfigureServices(services =>
{
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
services.AddRouting();
});
using var server = new TestServer(builder);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
Assert.Equal(AuthErrorMessage, ex.Message);
}
[Fact]
public async Task AuthorizationMiddleware_ConfiguredAfterRouting_Throws()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization());
app.UseAuthorization();
})
.ConfigureServices(services =>
{
services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build());
services.AddRouting();
});
using var server = new TestServer(builder);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
Assert.Equal(AuthErrorMessage, ex.Message);
}
[Fact]
public async Task CorsMiddleware_WithCorsEndpoint()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseRouting();
app.UseCors();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin()));
})
.ConfigureServices(services =>
{
services.AddCors();
services.AddRouting();
});
using var server = new TestServer(builder);
var response = await server.CreateRequest("/").SendAsync("PUT");
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task CorsMiddleware_ConfiguredBeforeRouting_Throws()
{
// Arrange
var builder = new WebHostBuilder();
builder.Configure(app =>
{
app.UseCors();
app.UseRouting();
app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin()));
})
.ConfigureServices(services =>
{
services.AddCors();
services.AddRouting();
});
using var server = new TestServer(builder);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.CreateRequest("/").SendAsync("GET"));
Assert.Equal(CORSErrorMessage, ex.Message);
}
}
}

View File

@ -10,6 +10,8 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Routing" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>

View File

@ -101,7 +101,8 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
Environment.NewLine +
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.";
"Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
@ -197,7 +198,8 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." +
Environment.NewLine +
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code.";
"Configure your application startup by adding app.UseCors() inside the call to Configure(..) in the application startup code. " +
"The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()

View File

@ -0,0 +1,10 @@
{
"solution": {
"path": "D:\\work\\aspnetcore\\src\\Middleware\\Middleware.sln",
"projects": [
"CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
"CORS\\test\\UnitTests\\Microsoft.AspNetCore.Cors.Test.csproj",
"CORS\\test\\testassets\\CorsMiddlewareWebSite\\CorsMiddlewareWebSite.csproj"
]
}
}

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
public class CorsMiddleware
{
// Property key is used by other systems, e.g. MVC, to check if CORS middleware has run
private const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";
private static readonly object CorsMiddlewareInvokedValue = new object();
private const string CorsMiddlewareWithEndpointInvokedKey = "__CorsMiddlewareWithEndpointInvoked";
private static readonly object CorsMiddlewareWithEndpointInvokedValue = new object();
private readonly Func<object, Task> OnResponseStartingDelegate = OnResponseStarting;
private readonly RequestDelegate _next;
@ -116,14 +116,6 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
/// <inheritdoc />
public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
{
// Flag to indicate to other systems, that CORS middleware was run for this request
context.Items[CorsMiddlewareInvokedKey] = CorsMiddlewareInvokedValue;
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
{
return _next(context);
}
// CORS policy resolution rules:
//
// 1. If there is an endpoint with IDisableCorsAttribute then CORS is not run
@ -131,9 +123,20 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
// there is an endpoint with IEnableCorsAttribute that has a policy name then
// fetch policy by name, prioritizing it above policy on middleware
// 3. If there is no policy on middleware then use name on middleware
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
// EndpointRoutingMiddleware uses this flag to check if the CORS middleware processed CORS metadata on the endpoint.
// The CORS middleware can only make this claim if it observes an actual endpoint.
context.Items[CorsMiddlewareWithEndpointInvokedKey] = CorsMiddlewareWithEndpointInvokedValue;
}
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin))
{
return _next(context);
}
// 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>();

View File

@ -918,7 +918,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
[Fact]
@ -936,12 +936,37 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
"DefaultPolicyName");
var httpContext = new DefaultHttpContext();
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableCorsAttribute("MetadataPolicyName"), new DisableCorsAttribute()), "Test endpoint"));
// Act
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareInvoked"));
Assert.Contains(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
[Fact]
public async Task Invoke_WithoutEndpoint_InvokeFlagSet()
{
// Arrange
var corsService = Mock.Of<ICorsService>();
var mockProvider = Mock.Of<ICorsPolicyProvider>();
var loggerFactory = NullLoggerFactory.Instance;
var middleware = new CorsMiddleware(
Mock.Of<RequestDelegate>(),
corsService,
loggerFactory,
"DefaultPolicyName");
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
// Act
await middleware.Invoke(httpContext, mockProvider);
// Assert
Assert.DoesNotContain(httpContext.Items, item => string.Equals(item.Key as string, "__CorsMiddlewareWithEndpointInvoked"));
}
}
}

View File

@ -13,9 +13,9 @@ namespace Microsoft.AspNetCore.Authorization
{
public class AuthorizationMiddleware
{
// Property key is used by other systems, e.g. MVC, to check if authorization middleware has run
private const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
private static readonly object AuthorizationMiddlewareInvokedValue = new object();
// Property key is used by Endpoint routing to determine if Authorization has run
private const string AuthorizationMiddlewareInvokedWithEndpointKey = "__AuthorizationMiddlewareWithEndpointInvoked";
private static readonly object AuthorizationMiddlewareWithEndpointInvokedValue = new object();
private readonly RequestDelegate _next;
private readonly IAuthorizationPolicyProvider _policyProvider;
@ -35,8 +35,12 @@ namespace Microsoft.AspNetCore.Authorization
var endpoint = context.GetEndpoint();
// Flag to indicate to other systems, e.g. MVC, that authorization middleware was run for this request
context.Items[AuthorizationMiddlewareInvokedKey] = AuthorizationMiddlewareInvokedValue;
if (endpoint != null)
{
// EndpointRoutingMiddleware uses this flag to check if the Authorization middleware processed auth metadata on the endpoint.
// The Authorization middleware can only make this claim if it observes an actual endpoint.
context.Items[AuthorizationMiddlewareInvokedWithEndpointKey] = AuthorizationMiddlewareWithEndpointInvokedValue;
}
// IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();