Search code examples
c#asp.net-coresignalrjwt

JWT Authentication Failed with SignalR


I have a REST API (core 2.1) which need to support a SPA, providing restful endpoints and some live-interactive features using SignalR;

The Hub/MVC routes are running on the same server, which is also the provider of the JWT token.

After loggin in, the client-side receives a JWT token, which places on the header for every REST request, otherwise it gets a 401 (This is working with the [Authorize] attribute).

At the client-side, the code below tries to connect to my /hub endpoint: new HubConnectionBuilder().withUrl(HUB_URL, { accessTokenFactory: () => this.getToken() })
And if I place [Authorize] at my Hub class, I get the following error (Without the authorization, the client can send and listen correctly):

WebSocket connection to 'wss://localhost:5001/hub?id=MY_ID&access_token=MY_TOKEN' failed: HTTP Authentication failed; no valid credentials available

The server logged failed authentications:

(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/hub? id=MY_ID&access_token=MY_TOKEN
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.3658ms 401

Differently from the those requests, using the SAME JWT TOKEN with the REST ones (Using [Authorize]), with the Header: Bearer XXXX instead of querystring, triggers the OnTokenValidated. The OnAuthenticationFailed is never triggered, even if the authentication fails:

(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/api/products application/json
(Triggered a console.log on AddJwtBearerOptions.Events.OnTokenValidated)
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[2]
Successfully validated the token.


Check below my ´Startup.cs`

ConfigureServices(IServiceCollection)

services.AddSignalR();
services.AddCors(option => option.AddPolicy("CorsPolicy", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().AllowCredentials()));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters{
        ValidateIssuer = true,
        ValidIssuer = Configuration["JWT:Issuer"],
        ValidateLifetime = true,
        ValidateAudience = false,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecurityKey"]))
    };
});

Configure(IApplicationBuilder)

app.UseAuthentication();
app.UseCors("CorsPolicy");
app.UseHttpsRedirection();
app.UseMvc();
app.UseSignalR(routes => {
     routes.MapHub<ApplicationHub>("/hub");
});

Solution

  • You also need to add this block inside the .AddJwtBearer section:

    // We have to hook the OnMessageReceived event in order to
    // allow the JWT authentication handler to read the access
    // token from the query string when a WebSocket or 
    // Server-Sent Events request comes in.
    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("/hub")))
            {
                // Read the token out of the query string
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
    

    This can be found here in the docs.