Search code examples
asp.netasp.net-coresignalrasp.net-identity

JWT + SignalR on ASP Core 3 resulting in 401 Unauthorized


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();

Solution

  • 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.