2

If I use http calls outside of signalr, such as with postman or httpclient, I am able to have my token validated successfully on the server. It's when I try to connect through my signalr hub that the token is not passing authorization.

Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: Bearer MyTokenFooBar

My service setup is:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
    services.AddControllers();
    services.AddHealthChecks();
    services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder => { builder.ConnectionString = _configuration.GetConnectionString("DefaultConnection"); }));
    services.AddIdentity<ApplicationUser, IdentityRole>(setup =>
    {
        // foo
    }).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.RequireHttpsMetadata = false;
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = _configuration["Jwt:Issuer"],
                ValidAudience = _configuration["Jwt:Audience"],
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateIssuerSigningKey = false,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])),
                ValidateLifetime = false
            };

            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var path = context.HttpContext.Request.Path;
                    if (!path.StartsWithSegments("/chat")) return Task.CompletedTask;
                    var accessToken = context.Request.Headers[HeaderNames.Authorization];
                    if (!string.IsNullOrWhiteSpace(accessToken) && context.Scheme.Name == JwtBearerDefaults.AuthenticationScheme)
                    {
                        context.Token = accessToken;
                    }

                    return Task.CompletedTask;
                }
            };
        });

    services.AddAuthorization();

    services.AddSignalR(options => { options.EnableDetailedErrors = true; });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(options =>
    {
        options.MapHealthChecks("/health");
        options.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
    app.UseSignalR(options => { options.MapHub<ChatHub>("/chat"); });
}

I use a basic http auth header for the initial connection, which will sign the user into identity and generate a jwt token as a response for use in future calls.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login()
{
    var (headerUserName, headerPassword) = GetAuthLoginInformation(HttpContext);

    var signInResult = await _signInManager.PasswordSignInAsync(headerUserName, headerPassword, false, false);
    if (!signInResult.Succeeded)
    {
        return Unauthorized();
    }

    var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SuperTopSecretKeyThatYouDoNotGiveOutEver!"));
    var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
    var jwt = new JwtSecurityToken(signingCredentials: signingCredentials);
    var handler = new JwtSecurityTokenHandler();
    var token = handler.WriteToken(jwt);
    return new OkObjectResult(token);
}

And my client (a console application) is setup to cache this token and use it in future signalr calls as such:

Get the token:

_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encoding.GetBytes($"{userName}:{password}")));
var response = await _client.SendAsync(request); // this goes to the login action posted above
_token = await response.Content.ReadAsStringAsync();

...

_hubConnection = new HubConnectionBuilder()
    .WithUrl(new Uri(_baseAddress, "chat"),
        options => { options.AccessTokenProvider = () => Task.FromResult(_token); }) // send the cached token back with every request
    .Build();

// here is where the error occurs. 401 unauthorized comes back from this call.
await _hubConnection.StartAsync();
mariocatch
  • 8,305
  • 8
  • 50
  • 71
  • may this helps https://stackoverflow.com/questions/46765756/how-to-authorize-signalr-core-hub-method-with-jwt – Darem May 28 '19 at 13:55
  • Thanks @Darem, but unfortunately I already have exactly that in my configuration above. – mariocatch May 28 '19 at 14:15
  • https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-2.2&tabs=dotnet#configure-bearer-authentication when I unterstand it correct its possible that your client do not support the header so its will be send as a query string. Maybe is this the problem in your case? Sorry for only make guesses. – Darem May 28 '19 at 14:27

1 Answers1

2

Resolved.

The issue was that I was overriding the OnMessageReceived handler of the JwtBearerHandler and then having it read the incoming token myself... but the token I was passing it included the prefix Bearer, which when parsed by the above handler did not match the known token for the existing user.

Simply removing my override of OnMessageReceived and letting AspNetCore's deafult implementation of the JwtBearerHandler do its job allowed the token parsing to work correctly.

mariocatch
  • 8,305
  • 8
  • 50
  • 71
  • 1
    For future people getting here, if you follow the current instruction of signalr with app.UseEndpoints you will end up with 401. Solution was to use app.UseSignalR for now even tho it is marked as deprecated. – Marius Schmack Oct 24 '19 at 12:31
  • 1
    This worked for me with app.UseSignalR too, no luck with UseEndpoints. As a person who spent a couple of hours with this issue I'd like to point out that the call to UseSignalR should be after calls to app.UseFileServer(); app.UseRouting(); and app.UseAuthorization(); – Maxim Zabolotskikh Jun 17 '20 at 12:45
  • 1
    I did exactly the same, plucked the token out of the auth header and assigned to context.token. Spent days trying to work out the issue then came here. THANKYOU – tinmac Jan 21 '21 at 18:11