This is a complete solution how to do that. You can download a working and updated sample of the code from Github:
Note: The purpose of this answer is not how to create a SignlR app, and how to manage users. This is shown in the docs and many other tutorials. But one thing is lacking and it is how to protect the SignalR hub's end points, and how to make the user's claims available in the hub in Blazor Server App. I did not find a single example of dong it with Blazor Server App. I sought some hints from the Blazor team, but to no avail...
Some clarification
Note: Authenticating your users in the front-end of your Blazor Server App will not make you authorized to access protected end-points on the Hub. You should treat the Hub as you treat a Web Api end-points, which requires you to pass an access token when you perform HTTP calls to it. As for instance, if you want to retrieve data from the WeatherForecastController in a Web Api to the FetchData page employing the HttpClient service, you need to pass the access token in the Authorization header
When you use WebAssembly app with API authentication, you can pass the access token to the Hub when the hub connection is created. It's easy, the docs has a sample code demonstrating this, and actually you don't have much to do in order to secure the Hub, and access......................, no even with this there are some issues to deal with because only the UserIdentifier can be accessed in the Hub, not all the user's claims.
However, the answer here is about Blazor Server App, and the solution is to pass the security cookie (".AspNetCore.Identity.Application") to the hub. So, the first step to solve the issue is to capture the cookie from the HttpContext before the Blazor SPA is being rendered, and pass the cookie to the Blazor App as a parameter sent to the App component. Now that the cookie is available in the Blazor App you can access it from the Chat page and pass it to the Hub. Note that, unlike the WebAssembly App sample with SignalR, all the ClaimPrincipal object is available in the Hub, and you can access all its claims, as for instance:
var user = Context.User.Identity.Name
var userid = Context.UserIdentifier
_Host.cshtml
@{
// Capture the security cookie when the the initial call
// to the Blazor is being executed. Initial call is when
// the user type the url of your Blazor App in the address
// bar and press enter, or when the user is being redirected
// from the Login form to your Blazor SPA
// See more here: https://stackoverflow.com/a/59538319/6152891
var cookie =
HttpContext.Request.Cookies[".AspNetCore.Identity.Application"];
}
<body>
@* Pass the captured Cookie to the App component as a paramter*@
<component type="typeof(App)" render-mode="Server" param-
Cookie="cookie" />
</body>
App.razor
@inject CookiesProvider CookiesProvider
@* code omitted here... *@
@code{
[Parameter]
public string Cookie { get; set; }
protected override Task OnInitializedAsync()
{
// Pass the Cookie parameter to the CookiesProvider service
// which is to be injected into the Chat component, and then
// passed to the Hub via the hub connection builder
CookiesProvider.Cookie = Cookie;
return base.OnInitializedAsync();
}
}
CookiesProvider.cs (Complete code)
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SignalRServerIdentityAuthentication
{
public class CookiesProvider
{
public string Cookie { get; set; }
}
}
Startup.ConfigureService
services.AddScoped<CookiesProvider>();
services.AddSignalR();
Startup.Configure
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapHub<ChatHub>("/chatHub");
endpoints.MapFallbackToPage("/_Host");
});
Note that the NavMenu contain an AuthorizeView component the object of which is to prevent a user from accessing the Chat component, unless she has been authenticated. Note also that the Chat page is protected with the Authorize attribute.
NavMenu.razor
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
<AuthorizeView>
<li class="nav-item px-3">
<NavLink class="nav-link" href="chat">
<span class="oi oi-chat" aria-hidden="true"></span> Chat
</NavLink>
</li>
</AuthorizeView>
Chat.razor (Complete code)
@page "/chat"
@attribute [Authorize]
@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.AspNetCore.SignalR
@using SignalRServerIdentityAuthentication.Hubs
@inject NavigationManager NavigationManager
@using System.Net.Http
@using System.Net.Http.Json
@using System;
@using System.Net.Http.Headers;
@using System.Threading.Tasks;
@using Microsoft.AspNetCore.Http.Connections;
@using System.Net
@implements IAsyncDisposable
<p>@messageForBoard</p>
<hr />
<div>
<label for="user">User:</label>
<span id="user">@userName</span>
</div>
<div class="form-group">
<label for="messageInput">Message:</label>
<input onfocus="this.select();" @ref="elementRef" id="messageInput" @bind="messageInput" class="form-control my-input"/>
</div>
<div>
<button @onclick="Send" disabled="@(!IsConnected)" class="btn btn-outline-
secondary">Send Message</button>
@if (UserList != null)
{
<select id="user-list" @bind="selectedUser">
<option value="">All.....</option>
@foreach (var user in UserList)
{
<option value="@user">@user</option>
}
</select>
}
</div>
<div>
<label for="messagesList">Public Message Board:</label>
<ul id="messagesList">
@foreach (var message in messages)
{
<li>@message</li>
}
</ul>
</div>
<div>
<label for="private-messages-list">Private Message Board:</label>
<ul id="private-messages-list">
@foreach (var message in privateMessages)
{
<li>@message</li>
}
</ul>
</div>
@code {
HubConnection hubConnection;
private List<string> messages = new List<string>();
private List<string> privateMessages = new List<string>();
private string messageForBoard;
private string userName;
private string messageInput;
private string selectedUser;
private List<string> UserList;
private ElementReference elementRef;
[Inject]
public CookiesProvider CookiesProvider { get; set; }
protected override async Task OnInitializedAsync()
{
var container = new CookieContainer();
var cookie = new Cookie()
{
Name = ".AspNetCore.Identity.Application",
Domain = "localhost",
Value = CookiesProvider.Cookie
};
container.Add(cookie);
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"), options =>
{
// Pass the security cookie to the Hub. This is the way to do
// that in your case. In other cases, you may need to pass
// an access token, but not here......
options.Cookies = container;
}).Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(() => StateHasChanged());
});
hubConnection.On<string>("ReceiveUserName", (name) =>
{
userName = name;
InvokeAsync(() => StateHasChanged());
});
hubConnection.On<string>("MessageBoard", (message) =>
{
messageForBoard = message;
InvokeAsync(() => StateHasChanged());
});
hubConnection.On<string, string>("ReceivePrivateMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
privateMessages.Add(encodedMsg);
InvokeAsync(() => StateHasChanged());
});
hubConnection.On<List<string>>("ReceiveInitializeUserList", ( list) =>
{
UserList = list ;
InvokeAsync(() => StateHasChanged());
});
await hubConnection.StartAsync();
await hubConnection.InvokeAsync("InitializeUserList");
}
protected override void OnAfterRender(bool firstRender)
{
elementRef.FocusAsync();
}
async Task Send() => await hubConnection.SendAsync("SendMessage",
selectedUser, messageInput);
public bool IsConnected => hubConnection.State ==
HubConnectionState.Connected;
public void Dispose()
{
hubConnection.DisposeAsync();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
}
Note that in order to pass a private message you need to have the UserIdentifier, but you'll also need to associate the user you want to post a private message with the UserIdentifier. You can simply store a list of UserIdentifiers in the Chat, and pass the reiured one. This of course poses some security risks and should be avoided. See my code how I deal with this. The user can only view a list of user names (yes, these are the emails of the connected users. Recall that in the database the UserName colum contains the user's email). You can of course change this to a more displayable values; your display name can be first name + last name, etc. It's up to you. Just remeber that you'll need to add a new claim for this. How to do that merits a new question...
Hubs/ChatHub.cs (Complete code needs some cleaning of unnecessary using statements)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel;
using System.Net.Http;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authentication;
using System.Net.Http.Json;
namespace SignalRServerIdentityAuthentication.Hubs
{
[Authorize()]
public class ChatHub : Hub
{
private static List<ConnectedUser> connectedUsers = new List<ConnectedUser>();
public async Task InitializeUserList()
{
var list = (from user in connectedUsers
select user.Name ).ToList();
await Clients.All.SendAsync("ReceiveInitializeUserList", list);
}
public async Task SendMessage(string userID, string message)
{
if (string.IsNullOrEmpty(userID)) // If All selected
{
await Clients.All.SendAsync("ReceiveMessage", Context.User.Identity.Name ?? "anonymous", message);
}
else
{
var userIdentifier = (from _connectedUser in connectedUsers
where _connectedUser.Name == userID
select _connectedUser.UserIdentifier).FirstOrDefault();
await Clients.User(userIdentifier).SendAsync("ReceivePrivateMessage",
Context.User.Identity.Name ?? "anonymous", message);
}
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var user = connectedUsers.Where(cu => cu.UserIdentifier == Context.UserIdentifier).FirstOrDefault();
var connection = user.Connections.Where(c => c.ConnectionID == Context.ConnectionId).FirstOrDefault();
var count = user.Connections.Count;
if(count == 1) // A single connection: remove user
{
connectedUsers.Remove(user);
}
if (count > 1) // Multiple connection: Remove current connection
{
user.Connections.Remove(connection);
}
var list = (from _user in connectedUsers
select new { _user.Name }).ToList();
await Clients.All.SendAsync("ReceiveInitializeUserList", list);
await Clients.All.SendAsync("MessageBoard",
$"{Context.User.Identity.Name} has left");
// await Task.CompletedTask;
}
public override async Task OnConnectedAsync()
{
var user = connectedUsers.Where(cu => cu.UserIdentifier == Context.UserIdentifier).FirstOrDefault();
if (user == null) // User does not exist
{
ConnectedUser connectedUser = new ConnectedUser
{
UserIdentifier = Context.UserIdentifier,
Name = Context.User.Identity.Name,
Connections = new List<Connection> { new Connection { ConnectionID = Context.ConnectionId } }
};
connectedUsers.Add(connectedUser);
}
else
{
user.Connections.Add(new Connection { ConnectionID = Context.ConnectionId });
}
// connectedUsers.Add(new )
await Clients.All.SendAsync("MessageBoard", $"{Context.User.Identity.Name} has joined");
await Clients.Client(Context.ConnectionId).SendAsync("ReceiveUserName", Context.User.Identity.Name);
}
}
}
Hubs/ConnectedUser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SignalRServerIdentityAuthentication.Hubs
{
public class ConnectedUser
{
public string Name { get; set; }
public string UserIdentifier { get; set; }
public List<Connection> Connections { get; set; }
}
public class Connection
{
public string ConnectionID { get; set; }
}
}