Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

signalr core (2.1) JWT authentication hub/negotiate 401 Unauthorized

so I have a .net core (2.1) API that uses JWT tokens for authentication. I can login and make authenticated calls successfully.

I am using React (16.6.3) for the client, which getting a JWT code and making authenticated calls to the API works.

I am trying to add signalr hubs to the site. If I do not put an [Authorize] attribute on the hub class. I can connect, send and receive messages (its a basic chathub at the moment).

when I add the [Authorize] attribute to the class, the React app will make an HttpPost to example.com/hubs/chat/negotiate . I would get a 401 status code. the Authorization: Bearer abc..... header would be passed up.

To build the hub in React I use:

const hubConn = new signalR.HubConnectionBuilder()
            .withUrl(`${baseUrl}/hubs/chat`, { accessTokenFactory: () => jwt })
            .configureLogging(signalR.LogLevel.Information)
            .build();

where the jwt variable is the token.

I have some setup for the authentication:

services.AddAuthentication(a =>
{
    a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.SaveToken = false;
    options.Audience = jwtAudience;

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,
    ValidIssuer = jwtIssuer,
    ValidAudience = jwtAudience,
    RequireExpirationTime = true,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey)),

};

// 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"];
        var authToken = context.Request.Headers["Authorization"].ToString();

        var token = !string.IsNullOrEmpty(accessToken) ? accessToken.ToString() : !string.IsNullOrEmpty(authToken) ?  authToken.Substring(7) : String.Empty;

        var path = context.HttpContext.Request.Path;

        // If the request is for our hub...
        if (!string.IsNullOrEmpty(token) && path.StartsWithSegments("/hubs"))
        {
            // Read the token out of the query string
            context.Token = token;
        }
        return Task.CompletedTask;
    }                    
};

});

the OnMessageReceived event does get hit and context.Token does get set to the JWT Token.

I can't figure out what I am doing wrong to be able to make authenticated calls for signalr core.


solution

I updated my code to use 2.2 (not sure if this was actually required).

so I spent some time looking at the source code, and the examples within:

https://github.com/aspnet/AspNetCore

I had a Signalr CORS issue which was solved with:

services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy",
                builder => builder
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials()
                    .SetIsOriginAllowed((host) => true) //allow all connections (including Signalr)
                );
        });

the important part being .SetIsOriginAllowed((host) => true) This allows all connections for both website and signalr cors access.

I had not added

services.AddAuthorization(options =>
    {
        options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
        {
            policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            policy.RequireClaim(ClaimTypes.NameIdentifier);
        });
    });

I had only used services.AddAuthentication(a =>

I took the following directly from the samples in github

            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    if (!string.IsNullOrEmpty(accessToken) &&
                        (context.HttpContext.WebSockets.IsWebSocketRequest || context.Request.Headers["Accept"] == "text/event-stream"))
                    {
                        context.Token = context.Request.Query["access_token"];
                    }
                    return Task.CompletedTask;
                }
            };   

Not sure if this was needed in the attribute, but the same used it on its hubs

    [Authorize(JwtBearerDefaults.AuthenticationScheme)]

with that I could not get multiple website and console apps to connect and communicate via signalr.

like image 730
Jon Avatar asked Jan 26 '19 16:01

Jon


People also ask

How do I associate a user with a connection in SignalR?

SignalR can be used with ASP.NET Core authentication to associate a user with each connection. In a hub, authentication data can be accessed from the HubConnectionContext.User property. Authentication allows the hub to call methods on all connections associated with a user. For more information, see Manage users and groups in SignalR.

How do I access authentication data in a SignalR hub?

In a hub, authentication data can be accessed from the HubConnectionContext.User property. Authentication allows the hub to call methods on all connections associated with a user. For more information, see Manage users and groups in SignalR.

Which order should I register the SignalR authentication middleware?

The order in which you register the SignalR and ASP.NET Core authentication middleware matters. Always call UseAuthentication before UseSignalR so that SignalR has a user on the HttpContext. In a browser-based app, cookie authentication allows your existing user credentials to automatically flow to SignalR connections.

How is JWT bearer authentication configured on the server?

On the server, bearer token authentication is configured using the JWT Bearer middleware:


1 Answers

To be used with [Authorize] you need to set the request header. Since web sockets do not support headers, the token is passed with the query string, which you correctly parse. The one thing that's missing is

context.Request.Headers.Add("Authorization", "Bearer " + token);

Or in your case probably context.HttpContext.Request.Headers.Add("Authorization", "Bearer " + token);

Example:

This is how i do it. On the client:

const signalR = new HubConnectionBuilder().withUrl(`${this.hubUrl}?token=${token}`).build();

On the server, in Startup.Configure:

app.Use(async (context, next) => await AuthQueryStringToHeader(context, next));
// ...
app.UseSignalR(r => r.MapHub<SignalRHub>("/hubUrl"));

Implementation of AuthQueryStringToHeader:

private async Task AuthQueryStringToHeader(HttpContext context, Func<Task> next)
{
    var qs = context.Request.QueryString;

    if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"]) && qs.HasValue)
    {
        var token = (from pair in qs.Value.TrimStart('?').Split('&')
                     where pair.StartsWith("token=")
                     select pair.Substring(6)).FirstOrDefault();

        if (!string.IsNullOrWhiteSpace(token))
        {
            context.Request.Headers.Add("Authorization", "Bearer " + token);
        }
    }

    await next?.Invoke();
}
like image 86
Markus Dresch Avatar answered Oct 20 '22 17:10

Markus Dresch