0

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.

Jaroslav Kadlec
  • 2,505
  • 4
  • 32
  • 43
  • 3
    1. The error happens because`SigninManger::SignInAsync()` actually sends a cookie over HTTP. However, there's no HTTP Response when using signalR (websocket). 2. Even you implement a custom logic and build a cookie/token and then send the credentials over websocket, be aware the User Principal is created when the websocket connection has been established, you have to replace the user principal dynamically(see [this thread](https://stackoverflow.com/questions/58387683). 3. As a walkaround, I would suggest try another approach: sign in the user before the SignalR connection has been established. – itminus Dec 10 '19 at 01:34
  • thanks for comment, I was researching multiple options of how to achieve my goal and as you say this one does not seems as proper one. Better to do it without SignalR. – Jaroslav Kadlec Dec 10 '19 at 19:33

2 Answers2

2

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. 
}
Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
0

Seems it does not make any sense to try perform login inside hub method. It's more convenient to add Controller into project and perform login operation there instead of inside SignalR Hub.

My motivation was to avoid necessity of having Controllers inside project as there were no need of them due application design but it just can't compensate difficulties which comes with that.

I achieved my goal by just following this link and using fetch(...).

Jaroslav Kadlec
  • 2,505
  • 4
  • 32
  • 43
  • 1
    If you don't use cookies then you don't require http nor a controller. Instead use [Bearer Token Authentication](https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1) – Erik Philips Dec 10 '19 at 19:35
  • Incredible. I though I read everything related and you're able to point me on another obvious topic I should go through before asking here in just few seconds :) Thanks. – Jaroslav Kadlec Dec 10 '19 at 19:36
  • Well considering the help page came out literally 6 days before your question, I'm not surprised you haven't read it :D – Erik Philips Dec 10 '19 at 19:37
  • 1
    Thanks, that would be great :) That thing with help articles releasing is just tax for trying to be on LTS version as fast as possible :) – Jaroslav Kadlec Dec 10 '19 at 19:39