1
votes

I'm trying to connect to a SignalR service from my blazor webassembly client but this fails I think on CORS. This is the code in my razor file.

m_connection = new HubConnectionBuilder()
    .WithUrl(myMircoServiceUrl, options =>
    {
       options.AccessTokenProvider = () => Task.FromResult(userService.Token);
    })
   .WithAutomaticReconnect()
   .Build();
await m_connection.StartAsync();

Then in the webassembly logging I see the following error:

Access to fetch at 'xxxx/negotiate?negotiateVersion=1' from origin 'http://localhost:5010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I added the following CORS policy in my Blazor server configuration and something similar in the microservice config:

        app.UseResponseCompression();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBlazorDebugging();
        }
        else
        {
            app.UseExceptionHandler(@"/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseCookiePolicy();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseCors(policy => policy
            .WithOrigins("http://localhost:5010")
            .AllowAnyHeader()
            .AllowAnyMethod());

        app.UseClientSideBlazorFiles<Client.Program>();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToClientSideBlazor<Client.Program>(@"index.html");
        });

Anybody got any idea what might be wrong?

Update 1

I now see the following error in the Chrome console:

dotnet.js:1 WebSocket connection to 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' failed: HTTP Authentication failed; no valid credentials available

Update 2

I removed the [Authorize] attribute from the SignalR hub and now it connects. And I can send messages to the hub. Problem is there is a reason for this attribute, because I don't want that people can subscribe to messages that are not for them

Update 3

Still no progress. Looking at pulling out the authentication to a seperate microservice using IdentityServer4. Last status is I have the following startup routines:

  • Microservice: gist.github.com/njannink/15595b77ffe1c0593be1a555fa37f83f
  • Blazor server: gist.github.com/njannink/7302a888110e24d199ea45b66da4f26b
  • Blazor client: gist.github.com/njannink/add2568cbf48c8b3c070ccd4f28fd127
4
When I look with fiddler at the networkt traffic I actually see that the authorization header isn't passed to the signalr urlNiek Jannink
I moved the CORS policies to the SignalR services and now I get the following error: WebSocket connection to 'ws://localhost:5000/hubs/posts?id=IuYH8BQpJ4DlfTbugrf5-A' failed: HTTP Authentication failed; no valid credentials availableNiek Jannink
Don't you forgot a call to UseSignaR after UseCors ?agua from mars
The SignalR hubs are not on the Blazor server, but they are located in a different microserviceNiek Jannink
So you need to connect to that microservice not to your webserver, or create a proxy on your webserver. else your application can't connect to your SignalR hub.agua from mars

4 Answers

1
votes

In my case, ASP.NET Core 2.2 I have an API from which I want to be able to use SignalR from the API to connect to my client application.

I have Projects for

  1. Web API
  2. IdentityServer4
  3. MVC Client

With ASP.NET Core Identity as the for user management

In order for your user to be authenticated you need to implement a IUserIdProvider like this

 public class IdBasedUserIdProvider : IUserIdProvider
 {
      public string GetUserId(HubConnectionContext connection)
      {
           //TODO: Implement USERID Mapper Here
           //throw new NotImplementedException();
           //return whatever you want to map/identify the user by here. Either ID/Email
           return connection.User.FindFirst("sub").Value;
      }
 }

With this I make sure I am pushing along the ID/Email to a method I am calling either from the Server or Client. Although I can always use the .User on the HubContext and it works fine.

In my Web API Startup.cs file I came up with

public void ConfigureServices(IServiceCollection services)
{
     services.AddCors(cfg =>
           {
                cfg.AddDefaultPolicy(policy =>
                {
                     policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
                     .AllowAnyHeader()
                     .AllowAnyMethod()
                     .AllowCredentials()
                     .SetIsOriginAllowed((_) => true)
                     .SetIsOriginAllowedToAllowWildcardSubdomains();
                });
           });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
           RoleManager<IdentityRole> roleManager){

    app.UseCors();

}

NOTE Configuration.GetSection("AuthServer:DomainBaseUrl").Get() retrieves the list of domains to allow CORS for from a config file.

And I did this configuration in My Client App COnfigureService Method

           services.AddCors(cfg =>
           {
                cfg.AddDefaultPolicy(policy => {
                     policy.AllowAnyHeader();
                     policy.AllowAnyMethod();
                     policy.SetIsOriginAllowed((host) => true);
                     policy.AllowAnyOrigin();
                });
           });

I hope this helps your situation.

1
votes

I've got the same errors with CORS and afterwards Websocket.
In my case the fallback longPolling was used as why the connection worked but the console logged the error HTTP Authentication failed; no valid credentials available.
If you use Identity Server JWT the following code solved the error for my case.
(The Code is from the Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication)

services.AddAuthentication()
    .AddIdentityServerJwt();
// insert:
 services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);
                
            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                
                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Important: Your Route has to start with hubs for the Options to trigger!
(see Line path.StartsWithSegments("/hubs")))

app.UseEndpoints(e =>
            {
                ...
                e.MapHub<ChatHub>("hubs/chat");
            });
0
votes

The best solution is indeed as Ismail Umer described using a seperate authentication service using something like IdentityServer4. And use this service in all other services. This is something I will do in a next iteration.

As short term solution I temporary moved the blazor server part into my api service and use a dual authentication method (JWT header or cookie).

        var key = Encoding.UTF8.GetBytes(m_configuration[@"SecurityKey"]);
        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = @"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true
                };
            })
            .AddCookie();

        // TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
        services.AddAuthorization(options =>
        {
            var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
                CookieAuthenticationDefaults.AuthenticationScheme,
                JwtBearerDefaults.AuthenticationScheme);
            defaultAuthorizationPolicyBuilder =
                defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
            options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
        });
0
votes

This is problem with Microsoft.AspNetCore.SignalR.Client 3.1.3. You can read about it here in comments.

You can wait for update or temporarly fix this issue:

  1. Disable negotiation
  2. Set WebSocket transport explicitly
  3. Modify query url
  4. Add OnMessageReceived handler

Client side:

  var token = await GetAccessToken();
  var hubConnection = new HubConnectionBuilder()
        .WithUrl($"/notification?access_token={token}", options =>
        {
            options.SkipNegotiation = true;
            options.Transports = HttpTransportType.WebSockets;
            options.AccessTokenProvider = GetAccessToken;

        })
        .Build();

Server side:

        public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            // ...
        })
        .AddJwtBearer(options =>
        {
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
                    {
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                },
            };
        });
    }