Solution: Custom SignalR Endpoints (Hubs) in a Blazor Server Application using Azure B2C When Deployed to Azure

Michael Washington 911 Reputation points MVP
2023-01-15T08:30:23.1666667+00:00

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);
            }
        }
    }
Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,593 questions
Azure SignalR Service
Azure SignalR Service
An Azure service that is used for adding real-time communications to web applications.
146 questions
{count} vote

1 answer

Sort by: Most helpful
  1. Hatvani Sándor 0 Reputation points
    2024-05-15T13:18:30.56+00:00

    Hi Michael,

    Thank you. Your solution works in VS 2022 but after installation on IIS I get this error at await hubConnection.StartAsync(); :
    Error: System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request).

    at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()

    Could you help me with what the problem might be, please?

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.