1

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.

Rhywden
  • 642
  • 7
  • 20

1 Answers1

0

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
            {
[...]
Rhywden
  • 642
  • 7
  • 20
  • 1
    "the access_token seems to be needed already at the negotiation phase " Did you solve that issue ? Is your code the correct and robust way to do that, or simply a workaround ? – enet Dec 19 '21 at 22:51
  • The code above is actually the proper way to do this. The token is already passed in the headers during negotiation but headers cannot be adjusted in that way during the actual connection, hence this middleware-workaround. The actual culprit was the missing `option.Audience` – Rhywden Dec 20 '21 at 13:45