DEV Community

Cover image for Mastering SignalR Hub Security with Custom Token Authentication in .NET 8
Jyotirmaya Sahu
Jyotirmaya Sahu

Posted on

Mastering SignalR Hub Security with Custom Token Authentication in .NET 8

Recently I was working on a web development project that required me to transfer data using WebSockets to implement real-time communication. It was a React.js project with a .NET backend.

While MSDN provides excellent top-level documentation, it often lacks the low-level details needed for advanced use cases.

One such scenario is authenticating a SignalR Hub using a custom token. Yes, a custom token, not a JWT or the default Bearer token. This article explores how to achieve this. By the end, you will have a SignalR Hub that requires authentication and uses a custom token.

The Custom Token

The custom token we will use is a Base64-encoded delimited string of user information in the format:

userId:userName
Enter fullscreen mode Exit fullscreen mode

From this token, we will extract the userId and userName to create claims.

Project Setup

Here are the basic steps to set up the project:

  1. Create a .NET project:

    dotnet new webapi
    
  2. Add SignalR service:
    In the Program.cs file, register the SignalR service while building the application:

    builder.Services.AddSignalR();
    
  3. Create a Hub:
    Create a directory named hubs and add a file named GameHub.cs. Implement the following:

    public class GameHub : Hub
    {
       public override Task OnConnectedAsync()
       {
           return base.OnConnectedAsync();
       }
    
       public override Task OnDisconnectedAsync(Exception? exception)
       {
           return base.OnDisconnectedAsync(exception);
       }
    }
    
  4. Map the Hub:
    Expose the GameHub as an endpoint in Program.cs:

    app.MapHub<GameHub>("/hubs/game");
    

Implementing Custom Token Authentication

To use a custom token and extract user information from it, we need a custom authentication scheme. In .NET, an authentication scheme is a named identifier that specifies a method or protocol used to authenticate users, like cookies, JWT bearer tokens, or Windows authentication. For this scenario, we’ll create a scheme called CustomToken.

Custom Authentication Scheme Implementation

  1. Define the Custom Token Scheme Options:

    public class CustomTokenSchemeOptions : AuthenticationSchemeOptions
    {
       public CustomTokenSchemeOptions()
       {
           Events = new CustomTokenEvents();
       }
    
       public new CustomTokenEvents Events
       {
           get => (CustomTokenEvents)base.Events!;
           set => base.Events = value;
       }
    }
    
  2. Define the Scheme Handler:
    The CustomTokenSchemeHandler contains the logic to validate tokens and extract user claims:

    public class CustomTokenSchemeHandler : AuthenticationHandler<CustomTokenSchemeOptions>
    {
       private new CustomTokenEvents Events => (CustomTokenEvents)base.Events!;
    
       public CustomTokenSchemeHandler(
           IOptionsMonitor<CustomTokenSchemeOptions> options,
           ILoggerFactory logger,
           UrlEncoder encoder) : base(options, logger, encoder) {}
    
       protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
       {
           var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
           await Events.MessageReceivedAsync(messageReceivedContext);
    
           var token = messageReceivedContext.Token ?? GetTokenFromQuery();
    
           if (token is null)
           {
               return AuthenticateResult.NoResult();
           }
    
           byte[] data = Convert.FromBase64String(token);
           string decodedString = Encoding.UTF8.GetString(data);
           string[] userInfoArray = decodedString.Split(":");
    
           var claims = new[]
           {
               new Claim(ClaimTypes.Name, userInfoArray[1]),
               new Claim(ClaimTypes.Sid, userInfoArray[0])
           };
           var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
           var ticket = new AuthenticationTicket(principal, Scheme.Name);
           return AuthenticateResult.Success(ticket);
       }
    
       private string? GetTokenFromQuery()
       {
           var accessToken = Context.Request.Query["access_token"].ToString();
           return string.IsNullOrEmpty(accessToken) ? null : accessToken;
       }
    }
    
  3. Configure the Authentication Scheme:
    Register the custom authentication scheme in Program.cs:

    builder.Services.AddAuthentication("CustomToken")
       .AddScheme<CustomTokenSchemeOptions, CustomTokenSchemeHandler>("CustomToken", opts =>
       {
           opts.Events = new CustomTokenEvents
           {
               OnMessageReceived = context =>
               {
                   var accessToken = context.Request.Query["access_token"];
                   var path = context.HttpContext.Request.Path;
    
                   if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/game"))
                   {
                       context.Token = accessToken;
                   }
    
                   return Task.CompletedTask;
               };
           };
       });
    

Disclaimer

The implementation presented in this article is inspired by the BearerTokenScheme source code in the .NET official repository. Adjustments were made to suit the custom token requirements for this scenario.

Wrapping Up

By implementing this custom token authentication scheme, you can secure your SignalR hubs and tailor the authentication process to your application's unique requirements. This approach allows for fine-grained control over token validation and claim extraction, ensuring a secure and robust real-time communication system.

Feel free to extend this implementation with additional validations, logging, or integrations with external identity providers for a more comprehensive solution.

Top comments (0)