1
votes

I've upgraded my project from ASP.Net Core 2.0 to ASP.NET Core 2.1 by following this tutorial.

Everything was fine until I applied Signar Core 2.1 to my project.

This is my Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IAuthorizationHandler, SolidAccountRequirementHandler>();

        services.AddCors(
            options => options.AddPolicy("AllowCors",
                builder =>
                {
                    builder
                        .AllowAnyOrigin()
                        .AllowCredentials()
                        .AllowAnyHeader()
                        .AllowAnyMethod();
                })
        );

        services.AddAuthorization(x =>
        {
            x.AddPolicy("MainPolicy", builder =>
            {
                builder.Requirements.Add(new SolidAccountRequirement());
            });
        });

        services.AddSignalR();

        #region Mvc builder

        var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);

        authenticationBuilder.AddJwtBearer(o =>
        {
            // You also need to update /wwwroot/app/scripts/app.js
            o.SecurityTokenValidators.Clear();

            // Initialize token validation parameters.
            var tokenValidationParameters = new TokenValidationParameters();
            tokenValidationParameters.ValidAudience = "audience";
            tokenValidationParameters.ValidIssuer = "issuer";
            tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SigningKey"));
            tokenValidationParameters.ValidateLifetime = false;

            o.TokenValidationParameters = tokenValidationParameters;
        });

        // Construct mvc options.
        services.AddMvc(mvcOptions =>
            {
                ////only allow authenticated users
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                    .AddRequirements(new SolidAccountRequirement())
                    .Build();

                mvcOptions.Filters.Add(new AuthorizeFilter(policy));
            })
            .AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); ;

        #endregion
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        //app.UseHttpsRedirection();
        app.UseCors("AllowCors");

        app.UseSignalR(routes =>
        {
            routes.MapHub<ChatHub>("/chathub");
        });

        app.UseMvc();
    }
}

This is my SolidRequirementHandler

public class SolidAccountRequirementHandler : AuthorizationHandler<SolidAccountRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SolidAccountRequirement requirement)
    {
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

This is my ChatHub.cs:

public class ChatHub : Hub
{
    [Authorize(Policy = "MainPolicy")]
    public override Task OnConnectedAsync()
    {
        return base.OnConnectedAsync();
    }
}

What I expected was MainPolicy would be called when I used my AngularJS app to connect to ChatHub. However, OnConnectedAsync() function was called without checking request identity.

The policy of MVC Controller was applied successfully, but Signalr's doesn't.

Can anyone help me please ?

Thank you,

2

2 Answers

4
votes

I posted this question onto Signalr github issue page. Here is the answer they gave me . I tried and it worked successfully:

The solution is to put [Authorize] attribute onto ChatHub

[Authorize(Policy = "MainPolicy")]
public class ChatHub : Hub
{
    public override Task OnConnectedAsync()
    {
        return base.OnConnectedAsync();
    }
}

Just share to who doesn't know :)

2
votes

I have the same problem, there are four key things:

1- In your Startup.cs be aware of this Order inside Configure(IApplicationBuilder app)

            app.UseRouting();
            app.UseAuthorization( );
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<myChat>("/chat");
            });

the app.UseAuthorization( ); should always be between app.UseRouting(); and app.UseEndpoints().

2- SignalR doesn't send Tokens in Header but it sends them in Query. In your startup.cs inside ConfigureServices(IServiceCollection services) You have to configure your app in a way to read Tokens from the query and put them in the header. You can Configure your JWT in this way:

  services.AddAuthentication()
            .AddJwtBearer(options =>
            {
                options.RequireHttpsMetadata = false;
                options.SaveToken = true;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidIssuer = [Issuer Site],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes([YOUR SECRET KEY STRING]))
                };
                options.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        var path = context.Request.Path;
                        var accessToken = context.Request.Query["access_token"];
                        if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
                        {
                            
                            context.Request.Headers.Add("Authorization", new[] { $"Bearer {accessToken}" });
                        }
                        return Task.CompletedTask;
                    }
                };
            });

3- Your Client should send Token when it wants to establish a connection. You can add token to Query when building the connection.

var connection = new signalR.HubConnectionBuilder().withUrl(
"http://localhost:5000/chat", {
    skipNegotiation: true,
    transport: signalR.HttpTransportType.WebSockets,
    accessTokenFactory: () => "My Token Is Here"}).build();

4- I didn't add a default Athuentication scheme inside services.AddAuthentication() So every time I have to specify my authorization scheme like this. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] And Finally, You can Protect your Chat Class Like this

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;

        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        public class myChat : Hub
        {
            ///Some functions
        }

It seems that Using statements is important, So make sure using the right ones. SignalR hub Authorize attribute doesn't work

Note: I have a problem with Authorizing only a single method in the myChat class. I don't know why.