Search code examples
c#asp.net-core.net-coreasp.net-core-signalr

Async login within hub method


Let's have web application from Visual Studio template using netcoreapp3.1. It uses asp net identity, e.g. page gets refreshed upon click on Login button.

What I'm trying to achieve is to have SignalR Core hub method like this

 [HttpGet]
 [AllowAnonymous]
 [ValidateAntiForgeryToken]
 public async Task<bool> Login(string email, string password)
 {
     var result = await _signInManager.PasswordSignInAsync(email,
                    password, true, lockoutOnFailure: false).ConfigureAwait(false);

      if (result.Succeeded)
      {
         return true;
      }
 ....
 ....
 }

unfortunately for my naive attempt I 'll get InvalidOperationException: Headers are read-only, response has already started. With horribly long stack trace ending with

   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
   at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.CookiePolicy.ResponseCookiesWrapper.Append(String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.<HandleSignInAsync>d__25.MoveNext()

I found that for similar use-cases it's common to interact with HttpContext but I can't find way how it could play role in this scenario as ApplicationSignInManager seemed relatively independant to that.

I realize it's quite possible I'm missing something from conceptual point of view so every idea about how to get closer to desired goal is welcome.

Seems to be described here github issue so I'll need to think about redesign probably.


Solution

  • You can achieve a controllerless model if you switch to Bearer Token authentication.

    All the following examples and code are from Authentication and authorization in ASP.NET Core SignalR.

    typescript connection

    // Connect, using the token we got.
    this.connection = new signalR.HubConnectionBuilder()
        .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
        .build();
    

    C# hub builder

    var connection = new HubConnectionBuilder()
        .WithUrl("https://example.com/myhub", options =>
        { 
            options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
        })
        .Build();
    

    The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.

    In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers in browsers when using some transports. When using WebSockets and Server-Sent Events, the token is transmitted as a query string parameter. To support this on the server, additional configuration is required:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
    
        services.AddAuthentication(options =>
            {
                // Identity made Cookie authentication the default.
                // However, we want JWT Bearer Auth to be the default.
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                // Configure the Authority to the expected value for your authentication provider
                // This ensures the token is appropriately validated
                options.Authority = /* TODO: Insert Authority URL here */;
    
                // 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.
    
                // Sending the access token in the query string is required due to
                // a limitation in Browser APIs. We restrict it to only calls to the
                // SignalR hub in this code.
                // See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
                // for more information about security considerations when using
                // the query string to transmit the access token.
                options.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        var accessToken = context.Request.Query["access_token"];
    
                        // If the request is for our hub...
                        var path = context.HttpContext.Request.Path;
                        if (!string.IsNullOrEmpty(accessToken) &&
                            (path.StartsWithSegments("/hubs/chat")))
                        {
                            // Read the token out of the query string
                            context.Token = accessToken;
                        }
                        return Task.CompletedTask;
                    }
                };
            });
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddSignalR();
    
        // Change to use Name as the user identifier for SignalR
        // WARNING: This requires that the source of your JWT token 
        // ensures that the Name claim is unique!
        // If the Name claim isn't unique, users could receive messages 
        // intended for a different user!
        services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
    
        // Change to use email as the user identifier for SignalR
        // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
    
        // WARNING: use *either* the NameUserIdProvider *or* the 
        // EmailBasedUserIdProvider, but do not use both. 
    }