Posting this in case I need it myself...
Issue: Using custom SignalR endpoints (for example a /chat hub) with Authentication in a Blazor Server application that uses Azure B2C will not work when deployed. It will work fine on your local machine (using IIS Express) but it won't work when deployed to IIS or Azure.
A Blazor Server app works fine, and you can add a custom hub, for example a chat app, and it will also work fine. However, once you try to enable authentication on your custom hub it won't work.
See:
Solution
Program.cs:
Normal set up from Tutorial: Build a Blazor Server chat app.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAutoMapper(typeof(Program).Assembly);
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
var app = builder.Build();
app.UseResponseCompression();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapHub<ChatHub>("/chathub");
app.MapFallbackToPage("/_Host");
app.Run();
}
}
_Host.cshtml:
This is the key. We need to grab all the Cookies at this point and put them in a collection of Cookies so they can be passed to the app.razor control.
<body>
@{
var CookieCollection = HttpContext.Request.Cookies;
Dictionary<string, string> Cookies = new Dictionary<string, string>();
foreach (var cookie in CookieCollection)
{
Cookies.Add(cookie.Key, cookie.Value);
}
}
<component type="typeof(App)" render-mode="ServerPrerendered" param-Cookies="Cookies" />
app.razor:
Here we get the collection of Cookies and set them as a CascadingValue that will be passed to all other Blazor controls.
<CascadingValue Name="Cookies" Value="Cookies">
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
</CascadingValue>
@code {
[Parameter] public Dictionary<string, string> Cookies { get; set; }
}
Index.razor:
Here we retrieve the collection of Cookies as a CascadingParameter and use those Cookies to manually set the Header and Cookies when creating the SignalR client.
@code {
#nullable disable
[CascadingParameter(Name = "Cookies")] public Dictionary<string, string> Cookies { get; set; }
System.Security.Claims.ClaimsPrincipal CurrentUser;
private HubConnection hubConnection;
private List<string> messages = new List<string>();
private string userInput;
private string messageInput;
private string strError = "";
protected override async Task OnInitializedAsync()
{
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
CurrentUser = authState.User;
// ** SignalR Chat
try
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"), options =>
{
options.UseDefaultCredentials = true;
var cookieCount = Cookies.Count();
var cookieContainer = new CookieContainer(cookieCount);
foreach (var cookie in Cookies)
cookieContainer.Add(new Cookie(
cookie.Key,
WebUtility.UrlEncode(cookie.Value),
path: "/",
domain: Navigation.ToAbsoluteUri("/").Host));
options.Cookies = cookieContainer;
foreach (var header in Cookies)
options.Headers.Add(header.Key, header.Value);
options.HttpMessageHandlerFactory = (input) =>
{
var clientHandler = new HttpClientHandler
{
PreAuthenticate = true,
CookieContainer = cookieContainer,
UseCookies = true,
UseDefaultCredentials = true,
};
return clientHandler;
};
})
.WithAutomaticReconnect()
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
}
catch(Exception ex)
{
strError = ex.Message;
}
}
catch
{
// do nothing if this fails
}
}
// ** SignalR Chat
private async Task Send()
{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendMessage", messageInput);
}
}
public bool IsConnected =>
hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
ChatHub.cs:
We can now add [Authorized] to the Hub, to prevent unauthorized access, and identify the user.
[Authorize]
public class ChatHub : Hub
{
public async override Task OnConnectedAsync()
{
if (this.Context.User?.Identity?.Name != null)
{
await Clients.All.SendAsync(
"broadcastMessage",
"_SYSTEM_",
$"{Context.User.Identity.Name} JOINED");
}
}
public async Task SendMessage(string message)
{
if (this.Context.User?.Identity?.Name != null)
{
await Clients.All.SendAsync(
"ReceiveMessage",
this.Context.User.Identity.Name,
message);
}
}
}