Search code examples
jwtsignalrblazoropenid-connectwebassembly

Blazor WebAssembly SignalR authentication not passing AccessToken upon negotiation


I'm trying to implement the authentication routine for a Blazor WASM application using SignalR and running into a wall, basically.

I've got an external Keycloak server up and running and the WASM application is successfully authenticating against that one; the client is actually getting a valid JWT token and all. It's when I try to get the SignalR Hub and the client to authenticate that I run into problems. As long as I don't add [Authenticate] to the Hub a connection is established, though.

According to the official docs, this is how I'm supposed to let the client connect to the hub:

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

And on the SignalR Hub I'm supposed to do this:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options =>
{
    options.Authority = "https://keycloak/auth/realms/master/";
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];

            // If the request is for our hub...
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) &&
                (path.StartsWithSegments("/chathub")))
            {
                // Read the token out of the query string
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

What I'm getting on the client is simply an error on the console with a big 401 (i.e. "Unauthorized") I was able to add a custom Authorization routine to the app (which simply returned "Success" for every auth attempt) and found out the probable root of the problem:

The client does two connection attempts to the Hub. The first one is to /chathub/negotiate?negotiateVersion=1 and the second one is to /chathub.

However, only the second request carries the access_token! As a result, using the above code will break at the first step because the access_token seems to be needed already at the negotiation phase for which the HubConnectionBuilder for some reason does not supply that parameter.

What am I doing wrong?

edit: See answer below. It's not a missing token which is the issue but rather a missing options.Audience setting.


Solution

  • Okay, I finally found the issue and the solution. I got annoyed at the fact that the token validation silently failed and then took a closer look at the middleware dealing with the token. I noticed that it basically overrode an event handler and asked myself if there were other event handlers?

    Well, lo and behold, adding OnAuthenticationFailed like this and setting a breakpoint on the return allowed me to see the actual error message:

    options.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        var accessToken = context.Request.Query["access_token"];
    
                        // If the request is for our hub...
                        var path = context.HttpContext.Request.Path;
                        if (!string.IsNullOrEmpty(accessToken) &&
                            (path.StartsWithSegments("/chathub")))
                        {
                            // Read the token out of the query string
                            context.Token = accessToken;
                        }
                        return Task.CompletedTask;
                    },
                    OnAuthenticationFailed = context =>
                    {
                        context.Response.StatusCode = 401;
                        return Task.CompletedTask;
                    }
                };
    

    which stated that the Audience property was null. All I now had to do was to add the proper mapping to my Keycloak server (see this StackOverflow thread ) and add options.Audience = "ClientId" to the configuration like this:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
            .AddJwtBearer(options =>
            {
                options.Authority = "https://keycloak/auth/realms/master";
                options.Audience = "ClientID";
                options.Events = new JwtBearerEvents
                {
    [...]