6
votes

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.

1
You say the token does get set, so it probably fails validation. You didn't include the token signing / transport code. Are you sure you're properly signing the token on the server side and properly storing/refreshing it on the client side?Eran
@Eran, I am sure the token is good, the react app makes authenticated calls with the JWT which is stored in local storage without an issue. the same token is used with the accessTokenFactory part when building the hubJon

1 Answers

6
votes

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