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.
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
{
[...]