I followed the below solution and moved code into library project. It works fine if I have hub connection in Blazor Server project but doesn't work when moved to library project:
Adding cookies to SignalR Core requests
I have below service in the Library Razor project, that hold implementation of hub connection setup and is shared among two Blazor server apps:
public class UserHubService : IAsyncDisposable
{
private HubConnection? _hubConnection;
public async Task<bool> InitializeHubAsync(string baseUrl, string hubPath, string subscribeToGroup, Guid? siteId, string userName)
{
UserName = userName;
var uri = new Uri(new Uri(baseUrl), hubPath);
//_hubConnection = new HubConnectionBuilder()
// .WithUrl(new Uri(new Uri(baseUrl), hubPath))
// .Build();
_hubConnection = new HubConnectionBuilder()
.WithUrl(uri, opti =>
{
if (_httpContextAccessor.HttpContext != null)
foreach (var c in _httpContextAccessor.HttpContext.Request.Cookies)
{
opti.Cookies.Add(new Cookie(c.Key, c.Value)
{
Domain = uri.Host, // Set the domain of the cookie
Path = "/" // Set the path of the cookie
});
}
})
.Build();
_hubConnection.On<Guid?, ProductModel, string>(SignalR_Method.TouchProductReceiveNotification, async (siteID, product, messageType) =>
{
if (_subscribedMessageTypes.Contains(messageType))
{
await HandleNotificationAsync(siteID, product, messageType);
}
});
try
{
await _hubConnection.StartAsync();
await _hubConnection.InvokeAsync(subscribeToGroup, siteId);
return true;
}
catch (Exception ex)
{
return false;
// Handle exception (e.g., log it)
}
}
}
The above service is injected in page and called from Server app:
await ProductNotificationHubService.InitializeHubAsync(baseUrl, hubPath, SignalR_Method.SubscribeToTouchSiteGroup, Site.Site_ID, _userInfo.UserName)
Below is the hub again in Library razor project:
[Authorize]
public class NotificationHub : Hub
{
private static readonly ConcurrentDictionary<string, List<string>> UserConnections = new();
[Authorize]
public override Task OnConnectedAsync()
{
var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
//var userName = Context.User?.Identity?.Name; // Assuming the username is stored in the Name claim
if (!string.IsNullOrEmpty(userEmail))
{
UserConnections.AddOrUpdate(
userEmail,
new List<string> { Context.ConnectionId }, // Add a new list with the current connection ID
(key, existingConnections) =>
{
if (!existingConnections.Contains(Context.ConnectionId))
{
existingConnections.Add(Context.ConnectionId); // Add the connection ID to the existing list
}
return existingConnections;
});
}
return base.OnConnectedAsync();
}
[Authorize]
public override Task OnDisconnectedAsync(Exception exception)
{
var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
var connectionID = Context.ConnectionId;
if (!string.IsNullOrEmpty(userEmail))
{
if (UserConnections.TryGetValue(userEmail, out var connections))
{
// Remove the specific connection ID
connections.Remove(Context.ConnectionId);
// If no more connections exist for this user, remove the user entry from the dictionary
if (connections.Count == 0)
{
UserConnections.TryRemove(userEmail, out _);
}
}
}
return base.OnDisconnectedAsync(exception);
}
}
I have confirmed that cookies are being set properly:
_hubConnection = new HubConnectionBuilder()
.WithUrl(uri, opti =>
{
if (_httpContextAccessor.HttpContext != null)
foreach (var c in _httpContextAccessor.HttpContext.Request.Cookies)
{
opti.Cookies.Add(new Cookie(c.Key, c.Value)
{
Domain = uri.Host, // Set the domain of the cookie
Path = "/" // Set the path of the cookie
});
}
})
.Build();
In the program.cs file:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
options.RequireAuthenticatedSignIn = true;
}).AddCookie(options =>
{
options.LoginPath = "/Account/Login/";
options.LogoutPath = "/Account/Logout/";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(30);
}).AddIdentityCookies();
app.UseAuthentication();
app.UseAuthorization();
The connection is successful and connected however, the issue I am having is that userEmail in below line is always null:
var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;
I have found the reason for context being null. I am connecting to hubconnection to send from one server app to another server app. Now for that I have entered URL of the other app which means the user is not authenticated on target URL. When I change url to source it shows context.
Now my question is that how would I authenticate that the user who is sending update to other app is registered user and secure signalr connection as well.
According to official recommendations, we'd better use Bearer token authentication.
Of course, we can also use Cookie authentication, but we need to pay attention to the browser's restriction that cookies cannot be shared between non-identical domain names.
Here is the sample code for you.
// Retrieve token from localStorage
var token = await _localStorageService.GetItemAsync("jwt_token");
var baseUrl = "https://localhost:7135"; // Replace with your actual domain
var hubPath = "/notificationHub"; // Path to your hub
_hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri(new Uri(baseUrl), hubPath), options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();