9

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.

Jon
  • 15,110
  • 28
  • 92
  • 132
  • 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 Feb 04 '19 at 15:08
  • @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 hub – Jon Feb 06 '19 at 17:15
  • The solution you wrote is working correctly, thanks – Aksoyhlc Mar 09 '23 at 08:46

2 Answers2

8

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();
}
Markus Dresch
  • 5,290
  • 3
  • 20
  • 40
  • unfortunately the Request.Headers already has the token in it. think I am going to have to create a vanilla app following tutorials to see if its some other config somewhere – Jon Feb 07 '19 at 17:51
  • @Jon, i added an example how i do it. – Markus Dresch Feb 08 '19 at 10:29
  • your answer was most the way there, so given you the points, expanded my question to explain how my outcome – Jon Feb 13 '19 at 15:04
  • 1
    Excellent solution! Minor fix, it should be `access_token=` and not `token=` – Rosdi Kasim Jun 20 '19 at 06:27
  • 3
    Warning: if the token expires there is no way for the signalR service to renew it when you add it directly in the url. Implement the accessTokenFactory which is the correct way of setting the token in the socket url. – Marcus Höglund Dec 18 '20 at 05:36
0

best method for using authorization for hubs is to force the application add the jwt token from the query string to the context and its working for me via this method

put this inside your program.cs (dot net 6):

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{



o.TokenValidationParameters = new TokenValidationParameters
{
    ValidIssuer = builder.Configuration["Jwt:Issuer"],
    ValidAudience = builder.Configuration["Jwt:Audience"],
    IssuerSigningKey = new SymmetricSecurityKey
        (Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = false,
    ValidateIssuerSigningKey = true
};

o.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var accessToken = context.Request.Query["access_token"];
        if (string.IsNullOrEmpty(accessToken) == false)
        {
            context.Token = accessToken;
        }

        return Task.CompletedTask;
    }
};


});

and my hubs are like this its using the main authorize method of asp

 [Microsoft.AspNetCore.Authorization.Authorize]
    public async Task myhub()
    {

       //do anything in your hub

    }

youtube totorial that helped me to solve this problem

Mamadmti
  • 61
  • 6