'Blazor Server SignalR hub fails on StartAsync due to Azure ADB2C

I created a Blazor server app in an effort to start learning Blazor and some newer technologies. I followed along with a hub chat tutorial that Microsoft has. Things worked great. I then added some basic cookie authentication and things were still on the up and up. Then I followed a tutorial from Carl Franklin on Blazor Train about connecting an app to Azure AD B2C. It works, except for the chat hub portion of the app. All of the other portions of the app work fine and show the user's info.

I'm almost positive that the "hubconnection" is failing because it's not authenticating because it's not getting the access token. The failure happens on the await hubConnection.StartAsync(); line with the error System.Text.Json.JsonReaderException: '<' is an invalid start of a value. I'm pretty sure that HTML is coming back with a 403 message or something like that.

So I guess I technically have 2 questions:

  1. How can I view the value that's causing the HubConnectionBuilder to error out? Even if I put in breakpoints I can never see what the value is that's causing it to choke.

  2. How can I pass the access token to the HubConnectionBuilder?

I've found lots of "ways" to do this that are either outdated or I couldn't make work:

Uses AddAzureADB2CBearer which is deprecated

Passes parameters to App which I can't get to work

Is for Azure AD, not B2C

This is what was working with Cookie auth:

    hubConnection = new HubConnectionBuilder()
    .WithUrl(
        NavigationManager.ToAbsoluteUri("/chathub"),
                config => config.UseDefaultCredentials = true)
    .Build();

And now it seems like I need to pass in an access token based off of this Microsoft page about Auth and SignalR but

hubConnection = new HubConnectionBuilder()
    .WithUrl(
            NavigationManager.ToAbsoluteUri("/chathub"), options =>
            {
                options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
            })
    .Build();

Here is what I'm doing in Startup to get B2C working based on Carl Franklin YouTube video

//****Azure B2C****//
        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"));
        

        services.AddControllersWithViews()
                .AddMicrosoftIdentityUI();

        services.AddAuthorization(options =>
        {
            // By default, all incoming requests will be authorized according to the default policy
            options.FallbackPolicy = options.DefaultPolicy;
        });

        services.AddRazorPages();
        services.AddServerSideBlazor()
                .AddMicrosoftIdentityConsentHandler();


Solution 1:[1]

I also had the same issue and managed to get it working. I had to manually pass through my authentication cookie to the Hub when connecting from my component.

This is my Hub Class:

[Authorize]
    public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub
    {
        public async override Task OnConnectedAsync() {
            var user = Context.User;
            var identifier = Context.UserIdentifier;
            string name = Context.User.Claims.First(x => x.Type.Equals("name")).Value;
            await base.OnConnectedAsync();
            await Clients.Caller.SendAsync("Connected", 0);
        }
    }

I then configured it in Program.cs

app.UseEndpoints(endpoints => {
    endpoints.MapControllers();
    endpoints.MapBlazorHub();
    endpoints.MapHub<NotificationsHub>("/notificationshub");
    endpoints.MapFallbackToPage("/_Host");
});

I also configured the Hub to use the UserId from my Microsoft cookie:

services.AddSingleton<IUserIdProvider, SignalRUserIdProvider>();
public class SignalRUserIdProvider : IUserIdProvider
    {
        public virtual string GetUserId(HubConnectionContext connection) {
            return connection.User?.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value!;
        }
    }

Finally, I passed through the cookie when connecting to my Hub. Example component:

@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.Extensions.Configuration
@implements IAsyncDisposable
@inject IHttpContextAccessor HttpContextAccessor;
@inject NavigationManager Navigate;

@code {
    private HubConnection hubConnection;

    protected override async Task OnInitializedAsync() {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigate.ToAbsoluteUri("/notificationshub"), options => {
                if (HttpContextAccessor.HttpContext != null) {
                    foreach (var cookie in HttpContextAccessor.HttpContext.Request.Cookies) {
                        // IF THE DOMAIN PARAMETER IS WRONG YOU WILL RECEIVE THE JSON ERROR.
                        options.Cookies.Add(new Cookie(cookie.Key, cookie.Value, null, domain: "localhost"));
                    }
                }
            })
            .WithAutomaticReconnect()
            .Build();

        hubConnection.On<int>("Connected", i => {
            // Yay, it worked
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    public async ValueTask DisposeAsync() {
        if (hubConnection is not null) {
            await hubConnection.DisposeAsync();
        }
    }
}

It is a long time since I had this issue, so please do let me know if anything is unclear.

Hope it helps!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 pbrotb