I'm attempting to set up SignalR with a .NET client and server.

I simply need a custom way to provide the ID of the connected client, and it seems the only way to do this is to populate the claims that are part of the HubConnectionContext argument when implementing the IUserIdProvider interface.

On my client I am constructing a JWT that works as intended (verified that it is constructed correctly).

On my server I have followed the exact instructions provided here

However, the OnMessageReceived callback does not fire, and therefore the token is ignored and the subsequently the claims array is empty in the IUserIdProvider.

It is worth noting that the IUserIdProvider still gets called when the client connects.

This is my client-side JWT generation code:

        _hubConnection = new HubConnectionBuilder()
            .WithUrl($"{_hubUrl}", options => options.AccessTokenProvider = () =>
            {
                var jwtHandler = new JwtSecurityTokenHandler();
                var credentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256);
                var jwt = new JwtSecurityToken(claims: new[] { new Claim("Id", _id) }, signingCredentials: credentials);
                var tokenString = jwtHandler.WriteToken(jwt);
                return Task.FromResult(tokenString);
            })
            .Build();

This is my Startup ConfigureServices function:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IUserIdProvider, MyIdProvider>();

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                LifetimeValidator = (before, expires, token, param) =>
                {
                    return true;
                },
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateActor = false,
                ValidateLifetime = true,
                IssuerSigningKey = SecurityKey
            };
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    // breakpoints never hit...
                    var accessToken = context.Request.Query["access_token"];
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrWhiteSpace(accessToken) &&
                        path.StartsWithSegments("myHub"))
                    {
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddSignalR();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc(Version, new OpenApiInfo
            {
                Version = Version,
                Title = Name
            });
        });            
    }

and then finally my IUserIdProvider implementation:

    public class MyIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        var id = connection.User.Claims.First(x => x.Type == "Id").Value;
        return id;
    }
}

Thanks in advance.

1 Answers

0
Stephen Foster On

There was a couple of problems that I had to solve to get this working.

Firstly the Hub requires the [Authorize] attribute otherwise OnMessageReceived will never be called.

Secondly, I was missing the app.UseAuthentication() that is also required for the pipeline to function correctly.

After making these changes, my claims were coming across correctly.