From 5ef51822dedb5ea34bb6a51f511a0123812bcb38 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 15 Feb 2019 12:32:01 -0800 Subject: [PATCH] Added endpoint routing support (#7608) - Basic endpoint routing support to for SignalR hubs, ConnectionHandler and IConnectionBuilder endpoints - Updated all functional tests and samples to use it - Added all attributes as metadata from Hubs and ConnectionHandlers - Added a test to verify client is rejected if auth is ineffective --- .../FunctionalTests/HubConnectionTests.cs | 38 +++++ .../Client/test/FunctionalTests/Hubs.cs | 6 + .../Client/test/FunctionalTests/Startup.cs | 59 +++---- .../clients/ts/FunctionalTests/Startup.cs | 46 +++--- ...onnectionEndpointRouteBuilderExtensions.cs | 151 ++++++++++++++++++ src/SignalR/samples/JwtSample/Startup.cs | 12 +- src/SignalR/samples/SignalRSamples/Startup.cs | 19 ++- src/SignalR/samples/SocialWeather/Startup.cs | 7 +- .../src/HubEndpointRouteBuilderExtensions.cs | 75 +++++++++ .../server/SignalR/test/MapSignalRTests.cs | 104 +++++++++++- src/SignalR/server/SignalR/test/Startup.cs | 10 +- .../server/StackExchangeRedis/test/Startup.cs | 6 +- 12 files changed, 456 insertions(+), 77 deletions(-) create mode 100644 src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs create mode 100644 src/SignalR/server/SignalR/src/HubEndpointRouteBuilderExtensions.cs diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index c0e3ff9daa..f351e66970 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -899,6 +899,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + [Theory] + [MemberData(nameof(TransportTypesWithAuth))] + public async Task ClientWillFailAuthEndPointIfNotAuthorized(HttpTransportType transportType, string hubPath) + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.Exception is HttpRequestException; + } + + using (StartServer(out var server, ExpectedErrors)) + { + var hubConnection = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + hubPath, transportType) + .Build(); + try + { + var ex = await Assert.ThrowsAnyAsync(() => hubConnection.StartAsync().OrTimeout()); + Assert.Equal("Response status code does not indicate success: 401 (Unauthorized).", ex.Message); + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + } + } + } + [Theory] [MemberData(nameof(TransportTypes))] public async Task ClientCanUseJwtBearerTokenForAuthenticationWhenRedirected(HttpTransportType transportType) @@ -1172,6 +1199,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + public static IEnumerable TransportTypesWithAuth() + { + foreach (var transport in TransportTypes().SelectMany(t => t).Cast()) + { + foreach (var path in new[] { "/authorizedhub", "/authorizedhub2" }) + { + yield return new object[] { transport, path }; + } + } + } + // This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests. public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" }; diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs index 7df3419539..af321348b5 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs @@ -221,4 +221,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { public string Echo(string message) => TestHubMethodsImpl.Echo(message); } + + // Authorization is added via endpoint routing in Startup + public class HubWithAuthorization2 : Hub + { + public string Echo(string message) => TestHubMethodsImpl.Echo(message); + } } diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs index c9a98272a5..13231ced52 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs @@ -5,9 +5,11 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; @@ -21,11 +23,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests public void ConfigureServices(IServiceCollection services) { - services.AddSignalR(options => - { - options.EnableDetailedErrors = true; - }) - .AddMessagePackProtocol(); + services.AddSignalR(options => options.EnableDetailedErrors = true) + .AddMessagePackProtocol(); + services.AddSingleton(); services.AddAuthorization(options => { @@ -35,50 +35,53 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests policy.RequireClaim(ClaimTypes.NameIdentifier); }); }); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.TokenValidationParameters = - new TokenValidationParameters + .AddJwtBearer(options => { - ValidateAudience = false, - ValidateIssuer = false, - ValidateActor = false, - ValidateLifetime = true, - IssuerSigningKey = SecurityKey - }; - }); + options.TokenValidationParameters = + new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateActor = false, + ValidateLifetime = true, + IssuerSigningKey = SecurityKey + }; + }); } public void Configure(IApplicationBuilder app) { app.UseAuthentication(); - app.UseSignalR(routes => + app.UseRouting(routes => { routes.MapHub("/default"); routes.MapHub("/dynamic"); routes.MapHub("/hubT"); routes.MapHub("/authorizedhub"); - routes.MapHub("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents); - }); + routes.MapHub("/authorizedhub2") + .RequireAuthorization(new AuthorizeAttribute(JwtBearerDefaults.AuthenticationScheme)); - app.Run(async (context) => - { - if (context.Request.Path.StartsWithSegments("/generateJwtToken")) + routes.MapHub("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents); + + routes.MapGet("/generateJwtToken", context => { - await context.Response.WriteAsync(GenerateJwtToken()); - return; - } - else if (context.Request.Path.StartsWithSegments("/redirect")) + return context.Response.WriteAsync(GenerateJwtToken()); + }); + + routes.Map("/redirect/{*anything}", context => { - await context.Response.WriteAsync(JsonConvert.SerializeObject(new + return context.Response.WriteAsync(JsonConvert.SerializeObject(new { url = $"{context.Request.Scheme}://{context.Request.Host}/authorizedHub", accessToken = GenerateJwtToken() })); - } + }); }); + + app.UseAuthorization(); } private string GenerateJwtToken() diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index a2a5cd4399..7f5cccf9e0 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; @@ -122,39 +123,21 @@ namespace FunctionalTests return next.Invoke(); }); - app.UseConnections(routes => - { - routes.MapConnectionHandler("/echo"); - }); - - app.Use(async (context, next) => - { - if (context.Request.Path.Value.Contains("/negotiate")) - { - context.Response.Cookies.Append("testCookie", "testValue"); - context.Response.Cookies.Append("testCookie2", "testValue2"); - context.Response.Cookies.Append("expiredCookie", "doesntmatter", new CookieOptions() { Expires = DateTimeOffset.Now.AddHours(-1) }); - } - await next.Invoke(); - }); - - app.UseSignalR(routes => + app.UseRouting(routes => { routes.MapHub("/testhub"); routes.MapHub("/testhub-nowebsockets", options => options.Transports = HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling); routes.MapHub("/uncreatable"); routes.MapHub("/authorizedhub"); - }); - app.Use(next => async (context) => - { - if (context.Request.Path.StartsWithSegments("/generateJwtToken")) + routes.MapConnectionHandler("/echo"); + + routes.MapGet("/generateJwtToken", context => { - await context.Response.WriteAsync(GenerateJwtToken()); - return; - } + return context.Response.WriteAsync(GenerateJwtToken()); + }); - if (context.Request.Path.StartsWithSegments("/deployment")) + routes.MapGet("/deployment", context => { var attributes = Assembly.GetAssembly(typeof(Startup)).GetCustomAttributes(); @@ -182,7 +165,20 @@ namespace FunctionalTests json.WriteTo(writer); } + + return Task.CompletedTask; + }); + }); + + app.Use(async (context, next) => + { + if (context.Request.Path.Value.Contains("/negotiate")) + { + context.Response.Cookies.Append("testCookie", "testValue"); + context.Response.Cookies.Append("testCookie2", "testValue2"); + context.Response.Cookies.Append("expiredCookie", "doesntmatter", new CookieOptions() { Expires = DateTimeOffset.Now.AddHours(-1) }); } + await next.Invoke(); }); } diff --git a/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs b/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..4aea5b9845 --- /dev/null +++ b/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs @@ -0,0 +1,151 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Connections.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing +{ + public static class ConnectionEndpointRouteBuilderExtensions + { + /// + /// Maps incoming requests with the specified path to the provided connection pipeline. + /// + /// The to add the route to. + /// The route pattern. + /// A callback to configure the connection. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapConnections(this IEndpointRouteBuilder builder, string pattern, Action configure) => + builder.MapConnections(pattern, new HttpConnectionDispatcherOptions(), configure); + + /// + /// Maps incoming requests with the specified path to the provided connection pipeline. + /// + /// The type. + /// The to add the route to. + /// The route pattern. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapConnectionHandler(this IEndpointRouteBuilder builder, string pattern) where TConnectionHandler : ConnectionHandler + { + return builder.MapConnectionHandler(pattern, configureOptions: null); + } + + /// + /// Maps incoming requests with the specified path to the provided connection pipeline. + /// + /// The type. + /// The to add the route to. + /// The route pattern. + /// A callback to configure dispatcher options. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapConnectionHandler(this IEndpointRouteBuilder builder, string pattern, Action configureOptions) where TConnectionHandler : ConnectionHandler + { + var options = new HttpConnectionDispatcherOptions(); + // REVIEW: WE should consider removing this and instead just relying on the + // AuthorizationMiddleware + var attributes = typeof(TConnectionHandler).GetCustomAttributes(inherit: true); + foreach (var attribute in attributes.OfType()) + { + options.AuthorizationData.Add(attribute); + } + configureOptions?.Invoke(options); + + var conventionBuilder = builder.MapConnections(pattern, options, b => + { + b.UseConnectionHandler(); + }); + + conventionBuilder.Add(e => + { + // Add all attributes on the ConnectionHandler has metadata (this will allow for things like) + // auth attributes and cors attributes to work seamlessly + foreach (var item in attributes) + { + e.Metadata.Add(item); + } + }); + + return conventionBuilder; + } + + + /// + /// Maps incoming requests with the specified path to the provided connection pipeline. + /// + /// The to add the route to. + /// The route pattern. + /// Options used to configure the connection. + /// A callback to configure the connection. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapConnections(this IEndpointRouteBuilder builder, string pattern, HttpConnectionDispatcherOptions options, Action configure) + { + var dispatcher = builder.ServiceProvider.GetRequiredService(); + + var connectionBuilder = new ConnectionBuilder(builder.ServiceProvider); + configure(connectionBuilder); + var connectionDelegate = connectionBuilder.Build(); + + // REVIEW: Consider expanding the internals of the dispatcher as endpoint routes instead of + // using if statemants we can let the matcher handle + + var conventionBuilders = new List(); + + // Build the negotiate application + var app = builder.CreateApplicationBuilder(); + app.UseWebSockets(); + app.Run(c => dispatcher.ExecuteNegotiateAsync(c, options)); + var negotiateHandler = app.Build(); + + var negotiateBuilder = builder.Map(pattern + "/negotiate", negotiateHandler); + conventionBuilders.Add(negotiateBuilder); + + // build the execute handler part of the protocol + app = builder.CreateApplicationBuilder(); + app.UseWebSockets(); + app.Run(c => dispatcher.ExecuteAsync(c, options, connectionDelegate)); + var executehandler = app.Build(); + + var executeBuilder = builder.Map(pattern, executehandler); + conventionBuilders.Add(executeBuilder); + + var compositeConventionBuilder = new CompositeEndpointConventionBuilder(conventionBuilders); + + // Add metadata to all of Endpoints + compositeConventionBuilder.Add(e => + { + // Add the authorization data as metadata + foreach (var data in options.AuthorizationData) + { + e.Metadata.Add(data); + } + }); + + return compositeConventionBuilder; + } + + private class CompositeEndpointConventionBuilder : IEndpointConventionBuilder + { + private readonly List _endpointConventionBuilders; + + public CompositeEndpointConventionBuilder(List endpointConventionBuilders) + { + _endpointConventionBuilders = endpointConventionBuilders; + } + + public void Add(Action convention) + { + foreach (var endpointConventionBuilder in _endpointConventionBuilders) + { + endpointConventionBuilder.Add(convention); + } + } + } + } +} diff --git a/src/SignalR/samples/JwtSample/Startup.cs b/src/SignalR/samples/JwtSample/Startup.cs index bead302db6..34d3dff880 100644 --- a/src/SignalR/samples/JwtSample/Startup.cs +++ b/src/SignalR/samples/JwtSample/Startup.cs @@ -66,11 +66,15 @@ namespace JwtSample public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseFileServer(); - app.UseSignalR(options => options.MapHub("/broadcast")); - var routeBuilder = new RouteBuilder(app); - routeBuilder.MapGet("generatetoken", c => c.Response.WriteAsync(GenerateToken(c))); - app.UseRouter(routeBuilder.Build()); + app.UseRouting(routes => + { + routes.MapHub("/broadcast"); + routes.MapGet("/generatetoken", context => + { + return context.Response.WriteAsync(GenerateToken(context)); + }); + }); } private string GenerateToken(HttpContext httpContext) diff --git a/src/SignalR/samples/SignalRSamples/Startup.cs b/src/SignalR/samples/SignalRSamples/Startup.cs index f8516fe9ae..dbb220f1d6 100644 --- a/src/SignalR/samples/SignalRSamples/Startup.cs +++ b/src/SignalR/samples/SignalRSamples/Startup.cs @@ -4,9 +4,11 @@ using System; using System.IO; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -55,23 +57,17 @@ namespace SignalRSamples app.UseCors("Everything"); - app.UseSignalR(routes => + app.UseRouting(routes => { routes.MapHub("/dynamic"); routes.MapHub("/default"); routes.MapHub("/streaming"); routes.MapHub("/uploading"); routes.MapHub("/hubT"); - }); - app.UseConnections(routes => - { routes.MapConnectionHandler("/chat"); - }); - app.Use(next => (context) => - { - if (context.Request.Path.StartsWithSegments("/deployment")) + routes.MapGet("/deployment", context => { var attributes = Assembly.GetAssembly(typeof(Startup)).GetCustomAttributes(); @@ -99,9 +95,12 @@ namespace SignalRSamples json.WriteTo(writer); } - } - return Task.CompletedTask; + + return Task.CompletedTask; + }); }); + + app.UseAuthorization(); } } } diff --git a/src/SignalR/samples/SocialWeather/Startup.cs b/src/SignalR/samples/SocialWeather/Startup.cs index b7721e3a2a..1ad19f35b6 100644 --- a/src/SignalR/samples/SocialWeather/Startup.cs +++ b/src/SignalR/samples/SocialWeather/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SocialWeather.Json; @@ -30,9 +31,13 @@ namespace SocialWeather app.UseDeveloperExceptionPage(); } - app.UseConnections(o => o.MapConnectionHandler("/weather")); app.UseFileServer(); + app.UseRouting(routes => + { + routes.MapConnectionHandler("/weather"); + }); + var formatterResolver = app.ApplicationServices.GetRequiredService(); formatterResolver.AddFormatter>("json"); formatterResolver.AddFormatter("protobuf"); diff --git a/src/SignalR/server/SignalR/src/HubEndpointRouteBuilderExtensions.cs b/src/SignalR/server/SignalR/src/HubEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..b32ca14149 --- /dev/null +++ b/src/SignalR/server/SignalR/src/HubEndpointRouteBuilderExtensions.cs @@ -0,0 +1,75 @@ +// 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.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing +{ + public static class HubEndpointRouteBuilderExtensions + { + /// + /// Maps incoming requests with the specified path to the specified type. + /// + /// The type to map requests to. + /// The to add the route to. + /// The route pattern. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapHub(this IEndpointRouteBuilder builder, string pattern) where THub : Hub + { + return builder.MapHub(pattern, configureOptions: null); + } + + /// + /// Maps incoming requests with the specified path to the specified type. + /// + /// The type to map requests to. + /// The to add the route to. + /// The route pattern. + /// A callback to configure dispatcher options. + /// An for endpoints associated with the connections. + public static IEndpointConventionBuilder MapHub(this IEndpointRouteBuilder builder, string pattern, Action configureOptions) where THub : Hub + { + var marker = builder.ServiceProvider.GetService(); + + if (marker == null) + { + throw new InvalidOperationException("Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code."); + } + + var options = new HttpConnectionDispatcherOptions(); + // REVIEW: WE should consider removing this and instead just relying on the + // AuthorizationMiddleware + var attributes = typeof(THub).GetCustomAttributes(inherit: true); + foreach (var attribute in attributes.OfType()) + { + options.AuthorizationData.Add(attribute); + } + + configureOptions?.Invoke(options); + + var conventionBuilder = builder.MapConnections(pattern, options, b => + { + b.UseHub(); + }); + + conventionBuilder.Add(e => + { + // Add all attributes on the Hub has metadata (this will allow for things like) + // auth attributes and cors attributes to work seamlessly + foreach (var item in attributes) + { + e.Metadata.Add(item); + } + }); + + return conventionBuilder; + } + } +} diff --git a/src/SignalR/server/SignalR/test/MapSignalRTests.cs b/src/SignalR/server/SignalR/test/MapSignalRTests.cs index e15e77a6dc..ab6aa928ef 100644 --- a/src/SignalR/server/SignalR/test/MapSignalRTests.cs +++ b/src/SignalR/server/SignalR/test/MapSignalRTests.cs @@ -1,8 +1,11 @@ -using System; +using System; +using System.Collections; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -36,7 +39,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests { executedConfigure = true; - var ex = Assert.Throws(() => { + var ex = Assert.Throws(() => + { app.UseSignalR(routes => { routes.MapHub("/overloads"); @@ -56,6 +60,43 @@ namespace Microsoft.AspNetCore.SignalR.Tests Assert.True(executedConfigure); } + [Fact] + public void NotAddingSignalRServiceThrowsWhenUsingEndpointRouting() + { + var executedConfigure = false; + var builder = new WebHostBuilder(); + + builder + .UseKestrel() + .ConfigureServices(services => + { + services.AddRouting(); + }) + .Configure(app => + { + executedConfigure = true; + + var ex = Assert.Throws(() => + { + app.UseRouting(routes => + { + routes.MapHub("/overloads"); + }); + }); + + Assert.Equal("Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code.", ex.Message); + }) + .UseUrls("http://127.0.0.1:0"); + + using (var host = builder.Build()) + { + host.Start(); + } + + Assert.True(executedConfigure); + } + [Fact] public void MapHubFindsAuthAttributeOnHub() { @@ -101,6 +142,49 @@ namespace Microsoft.AspNetCore.SignalR.Tests Assert.Equal(2, authCount); } + [Fact] + public void MapHubEndPointRoutingFindsAttributesOnHub() + { + var authCount = 0; + using (var host = BuildWebHostWithEndPointRouting(routes => routes.MapHub("/path", options => + { + authCount += options.AuthorizationData.Count; + }))) + { + host.Start(); + + var dataSource = host.Services.GetRequiredService(); + // We register 2 endpoints (/negotiate and /) + Assert.Equal(2, dataSource.Endpoints.Count); + Assert.NotNull(dataSource.Endpoints[0].Metadata.GetMetadata()); + Assert.NotNull(dataSource.Endpoints[1].Metadata.GetMetadata()); + } + + Assert.Equal(1, authCount); + } + + [Fact] + public void MapHubEndPointRoutingAppliesAttributesBeforeConventions() + { + void ConfigureRoutes(IEndpointRouteBuilder routes) + { + // This "Foo" policy should override the default auth attribute + routes.MapHub("/path") + .RequireAuthorization(new AuthorizeAttribute("Foo")); + } + + using (var host = BuildWebHostWithEndPointRouting(ConfigureRoutes)) + { + host.Start(); + + var dataSource = host.Services.GetRequiredService(); + // We register 2 endpoints (/negotiate and /) + Assert.Equal(2, dataSource.Endpoints.Count); + Assert.Equal("Foo", dataSource.Endpoints[0].Metadata.GetMetadata()?.Policy); + Assert.Equal("Foo", dataSource.Endpoints[1].Metadata.GetMetadata()?.Policy); + } + } + private class InvalidHub : Hub { public void OverloadedMethod(int num) @@ -126,6 +210,22 @@ namespace Microsoft.AspNetCore.SignalR.Tests { } + private IWebHost BuildWebHostWithEndPointRouting(Action configure) + { + return new WebHostBuilder() + .UseKestrel() + .ConfigureServices(services => + { + services.AddSignalR(); + }) + .Configure(app => + { + app.UseRouting(routes => configure(routes)); + }) + .UseUrls("http://127.0.0.1:0") + .Build(); + } + private IWebHost BuildWebHost(Action configure) { return new WebHostBuilder() diff --git a/src/SignalR/server/SignalR/test/Startup.cs b/src/SignalR/server/SignalR/test/Startup.cs index 1fe89590ac..2dd4b4b216 100644 --- a/src/SignalR/server/SignalR/test/Startup.cs +++ b/src/SignalR/server/SignalR/test/Startup.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.SignalR.Tests @@ -28,18 +29,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - app.UseConnections(routes => + app.UseRouting(routes => { + routes.MapHub("/uncreatable"); + routes.MapConnectionHandler("/echo"); routes.MapConnectionHandler("/echoAndClose"); routes.MapConnectionHandler("/httpheader"); routes.MapConnectionHandler("/auth"); }); - - app.UseSignalR(routes => - { - routes.MapHub("/uncreatable"); - }); } } } diff --git a/src/SignalR/server/StackExchangeRedis/test/Startup.cs b/src/SignalR/server/StackExchangeRedis/test/Startup.cs index c262eed003..5f102b6d7e 100644 --- a/src/SignalR/server/StackExchangeRedis/test/Startup.cs +++ b/src/SignalR/server/StackExchangeRedis/test/Startup.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; @@ -28,7 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - app.UseSignalR(options => options.MapHub("/echo")); + app.UseRouting(routes => + { + routes.MapHub("/echo"); + }); } private class UserNameIdProvider : IUserIdProvider