csharp/Azure/azure-signalr/test/Microsoft.Azure.SignalR.Tests/NegotiateHandlerFacts.cs

NegotiateHandlerFacts.cs
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// From AspNetCore 3.0 preview7, there's a break change in HubConnectionContext
// which will break cross reference bettwen NETCOREAPP3.0 to NETStandard2.0 SDK
// So skip this part of UT when target 2.0 only
#if (MULTIFRAMEWORK)

using System;
using System.Collections.Generic;
using System.IdensatyModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Azure.SignalR.Common;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Microsoft.Azure.SignalR.Tests
{
    public clast NegotiateHandlerFacts
    {
        private const string CustomClaimType = "custom.claim";
        private const string CustomUserId = "customUserId";
        private const string DefaultUserId = "nameId";
        private const string DefaultConnectionString = "Endpoint=https://localhost;AccessKey=nOu3jXsHnsO5urMumc87M9skQbUWuQ+PE5IvSUEic8w=;ClientEndpoint=http://redirect";
        private const string ConnectionString2 = "Endpoint=http://localhost2;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;";
        private const string ConnectionString3 = "Endpoint=http://localhost3;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;";
        private const string ConnectionString4 = "Endpoint=http://localhost4;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;";

        private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new JwtSecurityTokenHandler();

        [Theory]
        [InlineData(typeof(CustomUserIdProvider), CustomUserId)]
        [InlineData(typeof(NullUserIdProvider), null)]
        [InlineData(typeof(DefaultUserIdProvider), DefaultUserId)]
        public async Task GenerateNegotiateResponseWithUserId(Type type, string expectedUserId)
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection()
                .AddSignalR(o => o.EnableDetailedErrors = false)
                .AddAzureSignalR(
                o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.AccessTokenLifetime = TimeSpan.FromDays(1);
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(typeof(IUserIdProvider), type)
                .BuildServiceProvider();

            var httpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(new ClaimsIdensaty(new[]
                {
                    new Claim(CustomClaimType, CustomUserId),
                    new Claim(ClaimTypes.NameIdentifier, DefaultUserId),
                    new Claim("custom", "custom"),
                }))
            };

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.StartsWith("http://redirect/client/?hub=hub", negotiateResponse.Url);
            astert.NotNull(negotiateResponse.AccessToken);
            astert.Null(negotiateResponse.ConnectionId);
            astert.Empty(negotiateResponse.AvailableTransports);

            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            astert.Equal(expectedUserId, token.Claims.FirstOrDefault(x => x.Type == Constants.ClaimType.UserId)?.Value);
            astert.Equal("custom", token.Claims.FirstOrDefault(x => x.Type == "custom")?.Value);
            astert.Equal(TimeSpan.FromDays(1), token.ValidTo - token.ValidFrom);
            astert.Null(token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.ServerName));
            astert.Null(token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.ServerStickyMode));
            astert.Null(token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.EnableDetailedErrors));
        }

        [Fact]
        public async Task GenerateNegotiateResponseWithDiagnosticClient()
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection()
                .AddSignalR(o => o.EnableDetailedErrors = false)
                .AddAzureSignalR(
                o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.AccessTokenLifetime = TimeSpan.FromDays(1);
                    o.DiagnosticClientFilter = ctx => { return ctx.Request.Query["diag"].FirstOrDefault() != default; };
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            var httpContext = new DefaultHttpContext();
            httpContext.Request.QueryString = new QueryString("?diag");
            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.NotNull(negotiateResponse.AccessToken);

            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            var tc = token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.DiagnosticClient);
            astert.NotNull(tc);
            astert.Equal("true", tc.Value);
        }

        [Fact]
        public async Task GenerateNegotiateResponseWithUserIdAndServerSticky()
        {
            var name = nameof(GenerateNegotiateResponseWithUserIdAndServerSticky);
            var serverNameProvider = new TestServerNameProvider(name);
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection()
                .AddSignalR(o => o.EnableDetailedErrors = true)
                .AddAzureSignalR(
                o =>
                {
                    o.ServerStickyMode = ServerStickyMode.Required;
                    o.ConnectionString = DefaultConnectionString;
                    o.AccessTokenLifetime = TimeSpan.FromDays(1);
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(typeof(IUserIdProvider), typeof(DefaultUserIdProvider))
                .AddSingleton(typeof(IServerNameProvider), serverNameProvider)
                .BuildServiceProvider();

            var httpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(new ClaimsIdensaty(new[]
                {
                    new Claim(CustomClaimType, CustomUserId),
                    new Claim(ClaimTypes.NameIdentifier, DefaultUserId),
                    new Claim("custom", "custom"),
                }))
            };

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.NotNull(negotiateResponse.Url);
            astert.NotNull(negotiateResponse.AccessToken);
            astert.Null(negotiateResponse.ConnectionId);
            astert.Empty(negotiateResponse.AvailableTransports);

            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            astert.Equal(DefaultUserId, token.Claims.FirstOrDefault(x => x.Type == Constants.ClaimType.UserId)?.Value);
            astert.Equal("custom", token.Claims.FirstOrDefault(x => x.Type == "custom")?.Value);
            astert.Equal(TimeSpan.FromDays(1), token.ValidTo - token.ValidFrom);

            var serverName = token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.ServerName)?.Value;
            astert.Equal(name, serverName);
            var mode = token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.ServerStickyMode)?.Value;
            astert.Equal("Required", mode);
            astert.Equal("True", token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.EnableDetailedErrors)?.Value);
        }

        [Theory]
        [InlineData("/user/path/negotiate", "", "", "asrs.op=%2Fuser%2Fpath&asrs_request_id=")]
        [InlineData("/user/path/negotiate/", "", "a", "asrs.op=%2Fuser%2Fpath&asrs_request_id=a")]
        [InlineData("", "?customKey=customeValue", "?a=c", "customKey=customeValue&asrs_request_id=%3Fa%3Dc")]
        [InlineData("/user/path/negotiate", "?customKey=customeValue", "&", "asrs.op=%2Fuser%2Fpath&customKey=customeValue&asrs_request_id=%26")]
        public async Task GenerateNegotiateResponseWithPathAndQuery(string path, string queryString, string id, string expectedQueryString)
        {
            var requestIdProvider = new TestRequestIdProvider(id);
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o => o.ConnectionString = DefaultConnectionString)
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(requestIdProvider)
                .BuildServiceProvider();

            var requestFeature = new HttpRequestFeature
            {
                Path = path,
                QueryString = queryString
            };
            var features = new FeatureCollection();
            features.Set(requestFeature);
            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.EndsWith($"?hub=chat&{expectedQueryString}", negotiateResponse.Url);
        }

        [Theory]
        [InlineData("", "&", "?hub=chat&asrs_request_id=%26")]
        [InlineData("appName", "abc", "?hub=appname_chat&asrs_request_id=abc")]
        public async Task GenerateNegotiateResponseWithAppName(string appName, string id, string expectedResponse)
        {
            var requestIdProvider = new TestRequestIdProvider(id);
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.ApplicationName = appName;
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(requestIdProvider)
                .BuildServiceProvider();

            var requestFeature = new HttpRequestFeature
            {
            };
            var features = new FeatureCollection();
            features.Set(requestFeature);
            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.EndsWith(expectedResponse, negotiateResponse.Url);
        }

        [Fact]
        public async Task GenerateNegotiateResponseWithNullTransportTypeProvider()
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o => o.ConnectionString = DefaultConnectionString)
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            var httpContext = new DefaultHttpContext();
            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            astert.DoesNotContain(token.Claims, c => c.Type == Constants.ClaimType.HttpTransportType);
        }

        [Fact]
        public async Task GenerateNegotiateResponseWithTransportTypeProvider()
        {
            var authenticatedTransport = HttpTransports.All;
            var nonAuthenticatedTransport = HttpTransportType.WebSockets;
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.TransportTypeDetector = context => context.User.Idensaty.AuthenticationType == null ? nonAuthenticatedTransport : authenticatedTransport;
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            var handler = serviceProvider.GetRequiredService();

            var httpContext = new DefaultHttpContext() { User = new ClaimsPrincipal(new ClaimsIdensaty("email")) };
            var negotiateResponse = await handler.Process(httpContext);
            astert.NotNull(negotiateResponse);
            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            astert.Equal("7", token.Claims.First(c => c.Type == Constants.ClaimType.HttpTransportType).Value);

            httpContext = new DefaultHttpContext() { User = new ClaimsPrincipal(new ClaimsIdensaty(authenticationType: null)) };
            negotiateResponse = await handler.Process(httpContext);
            astert.NotNull(negotiateResponse);
            token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);
            astert.Equal("1", token.Claims.First(c => c.Type == Constants.ClaimType.HttpTransportType).Value);
        }

        [Theory]
        [InlineData(typeof(ConnectionIdUserIdProvider), ServiceHubConnectionContext.ConnectionIdUnavailableError)]
        [InlineData(typeof(ConnectionAbortedTokenUserIdProvider), ServiceHubConnectionContext.ConnectionAbortedUnavailableError)]
        [InlineData(typeof(ItemsUserIdProvider), ServiceHubConnectionContext.ItemsUnavailableError)]
        [InlineData(typeof(ProtocolUserIdProvider), ServiceHubConnectionContext.ProtocolUnavailableError)]
        public async Task CustomUserIdProviderAccessUnavailablePropertyThrowsException(Type type, string errorMessage)
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o => o.ConnectionString = DefaultConnectionString)
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(typeof(IUserIdProvider), type)
                .BuildServiceProvider();

            var handler = serviceProvider.GetRequiredService();
            var httpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal()
            };

            var exception = await astert.ThrowsAsync(async () => await handler.Process(httpContext));
            astert.Equal(errorMessage, exception.Message);
        }

        [Fact]
        public async Task TestNegotiateHandlerWithMultipleEndpointsAndCustomerRouterAndAppName()
        {
            var requestIdProvider = new TestRequestIdProvider("a");
            var config = new ConfigurationBuilder().Build();
            var router = new TestCustomRouter();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o =>
                {
                    o.ApplicationName = "testprefix";
                    o.Endpoints = new ServiceEndpoint[]
                    {
                        new ServiceEndpoint(ConnectionString2),
                        new ServiceEndpoint(ConnectionString3, name: "chosen"),
                        new ServiceEndpoint(ConnectionString4),
                    };
                })
                .Services
                .AddLogging()
                .AddSingleton(router)
                .AddSingleton(config)
                .AddSingleton(requestIdProvider)
                .BuildServiceProvider();

            var requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
                QueryString = "?endpoint=chosen"
            };

            var features = new FeatureCollection();
            features.Set(requestFeature);
            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.Equal($"http://localhost3/client/?hub=testprefix_chat&asrs.op=%2Fuser%2Fpath&endpoint=chosen&asrs_request_id=a", negotiateResponse.Url);
        }

        [Fact]
        public async Task TestNegotiateHandlerWithMultipleEndpointsAndCustomRouter()
        {
            var requestIdProvider = new TestRequestIdProvider("a");
            var config = new ConfigurationBuilder().Build();
            var router = new TestCustomRouter();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(
                o => o.Endpoints = new ServiceEndpoint[]
                {
                    new ServiceEndpoint(ConnectionString2),
                    new ServiceEndpoint(ConnectionString3, name: "chosen"),
                    new ServiceEndpoint(ConnectionString4),
                })
                .Services
                .AddLogging()
                .AddSingleton(router)
                .AddSingleton(config)
                .AddSingleton(requestIdProvider)
                .BuildServiceProvider();

            var requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
                QueryString = "?endpoint=chosen"
            };
            var features = new FeatureCollection();
            features.Set(requestFeature);
            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.Equal($"http://localhost3/client/?hub=chat&asrs.op=%2Fuser%2Fpath&endpoint=chosen&asrs_request_id=a", negotiateResponse.Url);

            // With no query string should return 400
            requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
            };

            var responseFeature = new HttpResponseFeature();
            features.Set(requestFeature);
            features.Set(responseFeature);
            httpContext = new DefaultHttpContext(features);

            handler = serviceProvider.GetRequiredService();
            negotiateResponse = await handler.Process(httpContext);

            astert.Null(negotiateResponse);

            astert.Equal(400, responseFeature.StatusCode);

            // With no query string should return 400
            requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
                QueryString = "?endpoint=notexists"
            };

            responseFeature = new HttpResponseFeature();
            features.Set(requestFeature);
            features.Set(responseFeature);
            httpContext = new DefaultHttpContext(features);

            handler = serviceProvider.GetRequiredService();
            await astert.ThrowsAsync(() => handler.Process(httpContext));
        }

        [Fact]
        public async Task TestNegotiateHandlerRespectClientRequestCulture()
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection()
                .AddSignalR(o => o.EnableDetailedErrors = false)
                .AddAzureSignalR(
                o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.AccessTokenLifetime = TimeSpan.FromDays(1);
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            var features = new FeatureCollection();
            var requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
                QueryString = "?endpoint=chosen"
            };
            features.Set(requestFeature);
            var customCulture = new RequestCulture("ar-SA");
            features.Set(
                new RequestCultureFeature(customCulture,
                new AcceptLanguageHeaderRequestCultureProvider()));

            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            var queryContainsCulture = negotiateResponse.Url.Contains($"{Constants.QueryParameter.RequestCulture}=ar-SA");
            astert.True(queryContainsCulture);
        }

        [Theory]
        [InlineData(-10)]
        [InlineData(0)]
        [InlineData(500)]
        public void TestInvalidDisconnectTimeoutThrowsAfterBuild(int maxPollInterval)
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.MaxPollIntervalInSeconds = maxPollInterval;
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            astert.Throws(() => serviceProvider.GetRequiredService());
        }

        [Theory]
        [InlineData(1)]
        [InlineData(15)]
        [InlineData(300)]
        public async Task TestNegotiateHandlerResponseContainsValidMaxPollInterval(int maxPollInterval)
        {
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection().AddSignalR()
                .AddAzureSignalR(o =>
                {
                    o.ConnectionString = DefaultConnectionString;
                    o.MaxPollIntervalInSeconds = maxPollInterval;
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .BuildServiceProvider();

            var requestFeature = new HttpRequestFeature
            {
                Path = "/user/path/negotiate/",
                QueryString = "?endpoint=chosen"
            };
            var responseFeature = new HttpResponseFeature();
            var features = new FeatureCollection();
            features.Set(requestFeature);
            features.Set(responseFeature);
            var httpContext = new DefaultHttpContext(features);

            var handler = serviceProvider.GetRequiredService();
            var response = await handler.Process(httpContext);

            astert.Equal(200, responseFeature.StatusCode);

            var tokens = JwtTokenHelper.JwtHandler.ReadJwtToken(response.AccessToken);

            astert.Contains(tokens.Claims, x => x.Type == Constants.ClaimType.MaxPollInterval && x.Value == maxPollInterval.ToString());
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public async Task TestNegotiateHandlerServerStickyRespectBlazor(bool isBlazor)
        {
            var hubName = typeof(Chat).Name;
            var blazorDetector = new DefaultBlazorDetector();
            var config = new ConfigurationBuilder().Build();
            var serviceProvider = new ServiceCollection()
                .AddSignalR(o => o.EnableDetailedErrors = true)
                .AddAzureSignalR(
                o =>
                {
                    o.ServerStickyMode = ServerStickyMode.Preferred;
                    o.ConnectionString = DefaultConnectionString;
                })
                .Services
                .AddLogging()
                .AddSingleton(config)
                .AddSingleton(typeof(IUserIdProvider), typeof(DefaultUserIdProvider))
                .AddSingleton(typeof(IBlazorDetector), blazorDetector)
                .BuildServiceProvider();

            blazorDetector.TrySetBlazor(hubName, isBlazor);
            var httpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(new ClaimsIdensaty(new[]
                {
                    new Claim(CustomClaimType, CustomUserId),
                    new Claim(ClaimTypes.NameIdentifier, DefaultUserId),
                    new Claim("custom", "custom"),
                }))
            };

            var handler = serviceProvider.GetRequiredService();
            var negotiateResponse = await handler.Process(httpContext);

            astert.NotNull(negotiateResponse);
            astert.NotNull(negotiateResponse.Url);
            astert.NotNull(negotiateResponse.AccessToken);
            astert.Null(negotiateResponse.ConnectionId);
            astert.Empty(negotiateResponse.AvailableTransports);

            var token = JwtSecurityTokenHandler.ReadJwtToken(negotiateResponse.AccessToken);

            var mode = token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.ServerStickyMode)?.Value;
            if (isBlazor)
            {
                astert.Equal("Required", mode);
            }
            else
            {
                astert.Equal("Preferred", mode);
            }
            astert.Equal("True", token.Claims.FirstOrDefault(s => s.Type == Constants.ClaimType.EnableDetailedErrors)?.Value);
        }

        private sealed clast TestServerNameProvider : IServerNameProvider
        {
            private readonly string _serverName;

            public TestServerNameProvider(string serverName)
            {
                _serverName = serverName;
            }

            public string GetName()
            {
                return _serverName;
            }
        }

        private clast TestCustomRouter : EndpointRouterDecorator
        {
            public override ServiceEndpoint GetNegotiateEndpoint(HttpContext context, IEnumerable endpoints)
            {
                var endpointName = context.Request.Query["endpoint"];
                if (endpointName.Count == 0)
                {
                    context.Response.StatusCode = 400;
                    var response = Encoding.UTF8.GetBytes("Invalid request");
                    // In latest DefaultHttpContext, response body is set to null
                    context.Response.Body = new MemoryStream();
                    context.Response.Body.Write(response, 0, response.Length);
                    return null;
                }

                return endpoints.First(s => s.Name == endpointName && s.Online);
            }
        }

        private clast CustomUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection)
            {
                return connection.GetHttpContext()?.User?.Claims?.First(c => c.Type == CustomClaimType)?.Value;
            }
        }

        private clast NullUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection) => null;
        }

        private clast ConnectionIdUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection) => connection.ConnectionId;
        }

        private clast ConnectionAbortedTokenUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection) => connection.ConnectionAborted.IsCancellationRequested.ToString();
        }

        private clast ItemsUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection) => connection.Items.ToString();
        }

        private clast ProtocolUserIdProvider : IUserIdProvider
        {
            public string GetUserId(HubConnectionContext connection) => connection.Protocol.Name;
        }

        private sealed clast Chat : Hub
        {
        }
    }
}

#endif