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
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:
-
Create a .NET project:
dotnet new webapi
-
Add SignalR service:
In theProgram.cs
file, register the SignalR service while building the application:
builder.Services.AddSignalR();
-
Create a Hub:
Create a directory namedhubs
and add a file namedGameHub.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); } }
-
Map the Hub:
Expose theGameHub
as an endpoint inProgram.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
-
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; } }
-
Define the Scheme Handler:
TheCustomTokenSchemeHandler
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; } }
-
Configure the Authentication Scheme:
Register the custom authentication scheme inProgram.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)