Custom SignalR hub authorization in ASP.NET Core
Image from Pexels

Custom SignalR hub authorization in ASP.NET Core

ASP.NET Core SignalR hub authorization

SignalR is number one choice for real-time communication between server and client. It implements several transports for communication between server and client. The biggest benefit is that sending messages from server to client is made easy and simple with out of the box fall-back to different transport depending on the client capabilities. 

In .NET 4.x SignalR was introduced as a separate NuGet package, but in .NET Core it became a part of ASP.NET Core framework, so no need for installing any additional packages when creating SignalR enabled ASP.NET Core web application. Since the library consists of both client and server component, we are going to focus only on server component in this article with accent to custom authorization.

Although SignalR comes with ASP.NET Core authorization wired up, there are always cases when you need to adapt to already existing infrastructure and security mechanisms. In this article we are going to focus exactly on this and I will try to explain custom authorization on a simple example where we'll only check for the query string parameter for authentication in order to keep things simple and focus on the actual custom authorization wiring up.

You can use attached sample solution which will be described further or you can just crate new ASP.NET Core application from Visual Studio 2019 using ASP.NET Core Web Application project template.

Note

Steps and custom solution n this article are based on ASP.NET Core 3.1. This may vary based on the version of ASP.NET Core you are building your project on top of

Custom authorization handler

Before we even start adding our SignalR message hub, we need to add classes that will be responsible for the hub authorization process. First, we need to create an empty implementation of IAuthorizationRequirement interface. This is only marker interface and there fore it does not declare any method or property.

using Microsoft.AspNetCore.Authorization;namespace Sample.CustomHubAuth.Authorization
{
    public class CustomAuthorizationRequirement: IAuthorizationRequirement
    {
    }
}

    

This class will be used as generic type for the custom authorization handler which will actually hold the authorization logic. It's main role is do bind our custom authorization handler with the authorization policy whic we will declare later when we register all services.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Threading.Tasks;

namespace Sample.CustomHubAuth.Authorization
{
    public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement>
    {
        readonly IHttpContextAccessor _httpContextAccessor;

        public CustomAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)
        {
            // Implement authorization logic
            if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue("username", out var username) &&
                username.Any() &&
                !string.IsNullOrWhiteSpace(username.FirstOrDefault()) &&
                username.FirstOrDefault() == "test_user")
            {
                // Authorization passed
                context.Succeed(requirement);
            }
            else
            {
                // Authorization failed
                context.Fail();
            }

            // Return completed task
            return Task.CompletedTask;
        }
    }
}

    

As I mentioned, I will use simple query string check which will be supplied during connecting to the hub from the client to authorize user. Of course, you should change this to use your custom authorization. To access HttpContext I am using injected singleton instance of IHttpContextAccessor implementation configured in the Startup.cs

Custom user id provider 

We have authorization handler in place, but that does not mean we know the username. In best case we would have user available from the ASP.NET MVC context but that does not have to be the case always. For example if you are authenticating with SWT (Simple Web Token) which is not bearer type of token, you may not have any info about the user. Instead you may have to go to authentication service to get additional user info for the token.

In this example we won't go that complicated and instead we will just really on the cookie value. Simple as possible.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;

namespace Sample.CustomHubAuth.Authorization
{
    public class CustomUserIdProvider : IUserIdProvider
    {
        readonly IHttpContextAccessor _httpContextAccessor;

        public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string GetUserId(HubConnectionContext connection)
        {
            // Implement user id acquiring logic
            return _httpContextAccessor.HttpContext.Request.Query["username"];
        }
    }
}

    

Username is important to be resolved in order to assign it to the connection. That way you can send messages only to specific users which are connected to your SignalR hub.

Strongly typed SignalR Hub

SignalR hub is not strongly typed by default. That means you get to work with dynamics which is always the most convenient way of writing your code, mostly because of the lack of strong types and auto complete from Visual Studio IDE and absence of compilation errors.

Note

This solution is built on top of .NET Core 3.1 LTS. Implementation on other version of .NET Core may vary a bit from these code snippets

It can easily happend that you mistype the method name when working with dynamic data type and you won't get compilation error. Everything starts but your messages never reach your front-end client because the method you typed simply does not exist on the front-end client.

For this reason I prefer to use typed SignalR hubs by referencing an interface with methods and models that front-end client implements and expects. This is simply dine by creating an interface which represents basically client capabilities in form of method and method parameters, but before we even define client interface we need to create models which will be accepted by the client.

using System;

namespace Sample.CustomHubAuth.Models
{
    public class NotificationModel
    {
        public String Title { get; set; }
        public String Content { get; set; }
    }
}

    

Now when we have the model, we can create the interface for the client

using Sample.CustomHubAuth.Models;
using System.Threading.Tasks;

namespace Sample.CustomHubAuth.Clients
{
    public interface INotificationsHubClient
    {
        Task Notify(NotificationModel notification);
    }
}
    

And when we have the client we can just reference it in the generic type for the SignalR hub.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Sample.CustomHubAuth.Clients;

namespace Sample.CustomHubAuth.Hubs
{
    [Authorize(Policy = "CustomHubAuthorizatioPolicy")]
    public class NotificationsHub: Hub<INotificationsHubClient>
    {

    }
}

    

This hub now will only support method declared in the client interface and therefore the possibility of mistyping the method name or the parameter is impossible as it will be checked during compilation time and Visual Studio 2019 IDE will report an error even before you try to compile the code, so you are safe.

Another thing you may notice on the hub is Authorize attribute which decorates the class. Attribute policy parameter in the constructor points to a policy which is declared in the dependency injection in Startup.cs. Since we pretty much have all the components in order to have secured communication with the client, we can proceed to wiring things in the dependency injection and pipeline methods of the Startup.cs class of our ASP.NET Core project.

Dependency injection and pipeline setup

All we have to do now is to wire up all the components using dependency injection. For more clarity I added comments on top of each section of dependency injection setup, so that you can clearly see the setup of services directly involved into putting SignalR in place.

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add CORS allowed domains
            services.AddCors(options =>
            {
                options.AddPolicy("HotificationsCorsPolicy",
                builder =>
                {
                    builder.WithOrigins("http://localhost:5000")
                        .AllowAnyHeader()
                        .AllowAnyMethod();
                });
            });

            // All lowercase routes
            services.AddRouting(options => options.LowercaseUrls = true);

            // MVC API controllers
            services.AddControllers();

            // Http context accessor singleton
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            // Add SignalR
            services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();
            services.AddAuthorization(options =>
            {
                options.AddPolicy("CustomHubAuthorizatioPolicy", policy =>
                {
                    policy.Requirements.Add(new CustomAuthorizationRequirement());
                });
            });
            services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
            services.AddSignalR();
        }
    

When our application main dependency injection container is sat up, we need to engage SignalR in the pipeline along with it's authorization.

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseCors("HotificationsCorsPolicy");

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            // Configure SignalR hub endpoint
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapHub<NotificationsHub>("/notifications", options =>
                {
                    options.Transports = HttpTransportType.WebSockets;
                });
            });

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    

Different projects may have different types of authentication and authorization which are not supported out of the box by ASP.NET Core SignalR and this is one of the ways to overcome this issue and adapt SignalR to use your custom security.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article