Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,6 +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.

namespace Microsoft.AspNetCore.Authorization
{
public partial interface IAllowAnonymous
{
}
public partial interface IAuthorizeData
{
string AuthenticationSchemes { get; set; }
string Policy { get; set; }
string Roles { get; set; }
}
}
namespace Microsoft.AspNetCore.Builder
{
public abstract partial class EndpointBuilder
Expand Down Expand Up @@ -87,6 +99,12 @@ public UsePathBaseMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Mic
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
}
}
namespace Microsoft.AspNetCore.Cors.Infrastructure
{
public partial interface ICorsMetadata
{
}
}
namespace Microsoft.AspNetCore.Http
{
public abstract partial class ConnectionInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Microsoft.AspNetCore.Authorization
{
/// <summary>
/// Marker interface to enable the <see cref="AllowAnonymousAttribute"/>.
/// Marker interface to allow access to anonymous users.
/// </summary>
public interface IAllowAnonymous
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public RouteOptions() { }
public System.Collections.Generic.ICollection<Microsoft.AspNetCore.Routing.EndpointDataSource> EndpointDataSources { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public bool LowercaseQueryStrings { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool LowercaseUrls { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressCheckForUnhandledSecurityMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class RouteValueEqualityComparer : System.Collections.Generic.IEqualityComparer<object>
{
Expand Down
63 changes: 49 additions & 14 deletions src/Http/Routing/src/EndpointMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
// 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;
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;

namespace Microsoft.AspNetCore.Routing
{
internal sealed class EndpointMiddleware
{
internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these should be features... Then we could back them by fields in Kestrel and avoid the dictionary look up altogether

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use mvc without ever initilizing Items? Most middleware use features instead. Note adding a feature would reset the version cache.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareInvoked";

private readonly ILogger _logger;
private readonly RequestDelegate _next;
private readonly RouteOptions _routeOptions;

public EndpointMiddleware(ILogger<EndpointMiddleware> logger, RequestDelegate next)
public EndpointMiddleware(
ILogger<EndpointMiddleware> logger,
RequestDelegate next,
IOptions<RouteOptions> routeOptions)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}

if (next == null)
{
throw new ArgumentNullException(nameof(next));
}

_logger = logger;
_next = next;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_next = next ?? throw new ArgumentNullException(nameof(next));
_routeOptions = routeOptions?.Value ?? throw new ArgumentNullException(nameof(routeOptions));
}

public async Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
if (endpoint?.RequestDelegate != null)
{
if (_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
// User opted out of this check.
return;
}

if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
!httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
{
ThrowMissingAuthMiddlewareException(endpoint);
}

if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
!httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
{
ThrowMissingCorsMiddlewareException(endpoint);
}

Log.ExecutingEndpoint(_logger, endpoint);

try
Expand All @@ -52,6 +71,22 @@ public async Task Invoke(HttpContext httpContext)
await _next(httpContext);
}

private static void ThrowMissingAuthMiddlewareException(Endpoint endpoint)
{
throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " +
Comment thread
pranavkm marked this conversation as resolved.
"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.");
}

private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint)
{
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.");
}

private static class Log
{
private static readonly Action<ILogger, string, Exception> _executingEndpoint = LoggerMessage.Define<string>(
Expand Down
20 changes: 20 additions & 0 deletions src/Http/Routing/src/RouteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ public class RouteOptions
/// </summary>
public bool AppendTrailingSlash { get; set; }

/// <summary>
/// Gets or sets a value that indicates if the check for unhandled security endpoint metadata is suppressed.
/// <para>
/// Endpoints can be associated with metadata such as authorization, or CORS, that needs to be
/// handled by a specific middleware to be actionable. If the middleware is not configured, such
/// metadata will go unhandled.
/// </para>
/// <para>
/// When <see langword="false"/>, prior to the execution of the endpoint, routing will verify that
/// all known security-specific metadata has been handled.
/// Setting this property to <see langword="true"/> suppresses this check.
/// </para>
/// </summary>
/// <value>Defaults to <see langword="false"/>.</value>
/// <remarks>
/// This check exists as a safeguard against accidental insecure configuration. You may suppress
/// this check if it does not match your application's requirements.
/// </remarks>
public bool SuppressCheckForUnhandledSecurityMetadata { get; set; }

private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();

public IDictionary<string, Type> ConstraintMap
Expand Down
154 changes: 150 additions & 4 deletions src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
// 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;
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.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Routing
{
public class EndpointMiddlewareTest
{
private readonly IOptions<RouteOptions> RouteOptions = Options.Create(new RouteOptions());

[Fact]
public async Task Invoke_NoFeature_NoOps()
{
Expand All @@ -24,7 +30,7 @@ public async Task Invoke_NoFeature_NoOps()
return Task.CompletedTask;
};

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next, RouteOptions);

// Act
await middleware.Invoke(httpContext);
Expand All @@ -49,7 +55,7 @@ public async Task Invoke_NoEndpoint_NoOps()
return Task.CompletedTask;
};

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next, RouteOptions);

// Act
await middleware.Invoke(httpContext);
Expand Down Expand Up @@ -81,7 +87,7 @@ public async Task Invoke_WithEndpoint_InvokesDelegate()
return Task.CompletedTask;
};

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next, RouteOptions);

// Act
await middleware.Invoke(httpContext);
Expand All @@ -90,6 +96,146 @@ public async Task Invoke_WithEndpoint_InvokesDelegate()
Assert.True(invoked);
}

[Fact]
public async Task Invoke_WithEndpoint_ThrowsIfAuthAttributesWereFound_ButAuthMiddlewareNotInvoked()
{
// 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.";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<IAuthorizeData>()), "Test"),
});

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, RouteOptions);

// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext));

// Assert
Assert.Equal(expected, ex.Message);
}

[Fact]
public async Task Invoke_WithEndpoint_WorksIfAuthAttributesWereFound_AndAuthMiddlewareInvoked()
{
// Arrange
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<IAuthorizeData>()), "Test"),
});

httpContext.Items[EndpointMiddleware.AuthorizationMiddlewareInvokedKey] = true;

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, RouteOptions);

// Act & Assert
await middleware.Invoke(httpContext);

// If we got this far, we can sound the everything's OK alarm.
}

[Fact]
public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledAuthAttributesWereFound_ButSuppressedViaOptions()
{
// Arrange
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<IAuthorizeData>()), "Test"),
});
var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true });
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, routeOptions);

// Act & Assert
await middleware.Invoke(httpContext);
}

[Fact]
public async Task Invoke_WithEndpoint_ThrowsIfCorsMetadataWasFound_ButCorsMiddlewareNotInvoked()
{
// 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.";
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<ICorsMetadata>()), "Test"),
});

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, RouteOptions);

// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext));

// Assert
Assert.Equal(expected, ex.Message);
}

[Fact]
public async Task Invoke_WithEndpoint_WorksIfCorsMetadataWasFound_AndCorsMiddlewareInvoked()
{
// Arrange
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<ICorsMetadata>()), "Test"),
});

httpContext.Items[EndpointMiddleware.CorsMiddlewareInvokedKey] = true;

var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, RouteOptions);

// Act & Assert
await middleware.Invoke(httpContext);

// If we got this far, we can sound the everything's OK alarm.
}

[Fact]
public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledCorsAttributesWereFound_ButSuppressedViaOptions()
{
// Arrange
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceProvider()
};

httpContext.Features.Set<IEndpointFeature>(new EndpointSelectorContext()
{
Endpoint = new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(Mock.Of<IAuthorizeData>()), "Test"),
});
var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true });
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, _ => Task.CompletedTask, routeOptions);

// Act & Assert
await middleware.Invoke(httpContext);
}

private class ServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,6 @@ public partial class DefaultCorsPolicyProvider : Microsoft.AspNetCore.Cors.Infra
public DefaultCorsPolicyProvider(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions> options) { }
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy> GetPolicyAsync(Microsoft.AspNetCore.Http.HttpContext context, string policyName) { throw null; }
}
public partial interface ICorsMetadata
{
}
public partial interface ICorsPolicyMetadata : Microsoft.AspNetCore.Cors.Infrastructure.ICorsMetadata
{
Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy Policy { get; }
Expand Down
1 change: 1 addition & 0 deletions src/Middleware/CORS/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cors.Test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
</ItemGroup>
Expand Down
Loading