SignalR ApplicationUserManager is disposed while using webSockets transport

Viewed 834

I've added SignalR to my existing MVC project and tried to use the same ApplicationUserManager from MVC to load user into hub context.

After connection is done, I call some server method like this:

        var alertsHub = $.connection.monitoringHub;

        $(function() {
            alertsHub.client.processAlertChange = function (name, alert) {
                console.log(alert);
            };

            //start the connection
            $.connection.hub.start().done(function () {
                console.log("Connected, transport = " + $.connection.hub.transport.name);
                alertsHub.server.subscribeToMonitor();
            });
        });

In my Hub method there is a call

CurrentUser = ConnectedUsers.GetOrAdd(this.Context.ConnectionId, UserManager.FindByName(this.Context.User.Identity.Name));

where UserManager is property just like this

        protected static ConcurrentDictionary<string, ApplicationUser> ConnectedUsers = new ConcurrentDictionary<string, ApplicationUser>();

    private ApplicationUserManager _userManager;
    public ApplicationUserManager UserManager
    {
        get
        {
            if (_userManager == null)
            {
                _userManager = Context.Request.GetHttpContext().GetOwinContext().GetUserManager<ApplicationUserManager>();
            }
            return _userManager;
        }
    }
    protected ApplicationUser CurrentUser { get; set; }

The problem is that when webSockets transport is used I'm getting an error 'Cannot access disposed object'

SignalR: webSockets transport connected. Initiating start request.
SignalR: The start request succeeded. Transitioning to the connected state.
SignalR: Now monitoring keep alive with a warning timeout of 13333.333333333332, keep alive timeout of 20000 and disconnecting timeout of 300000
Connected to monitoringHub, transport = webSockets
SignalR: Invoking monitoringhub.SubscribeToMonitor
SignalR: monitoringhub.SubscribeToMonitor failed to execute. Error: Cannot access a disposed object.
Object name: 'ApplicationUserManager'.

BUT! When I force serverSentEvents transport - there is no error and everything works great. Why? And how it can be fixed...

My Startup.cs

            ConfigureAuth(app);

        // Any connection or hub wire up and configuration should go here
        var hubConfiguration = new HubConfiguration();
        hubConfiguration.EnableDetailedErrors = true;

        app.MapSignalR("/sr", hubConfiguration);

        GlobalHost.HubPipeline.RequireAuthentication();
        GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(120);
        GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(300);
        GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(10);
3 Answers

Looks like you use auth token (Bearer or something else). SignalR is not sending auth token to the server by default. So, Context is empty. See Microsoft documentation on https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0

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

After you will add accessTokenFactory (on the client side), SignalR will include token value to the Request.Query param as 'access_token' and there is no possibility to change 'access_token' name to another name (e.g. 'token'). So, you need to implement an additional handler or middleware to auth and fill Context on your backend.

context.Request.Query["access_token"]

You can try the following steps:

  1. Turn on Streaming in Fiddler.
  2. If your SignalR transport is WebSockets, requests may take a few seconds to come through while Fiddler is on.

With a WebSockets SignalR connection, the initial tcp connection is upgraded to a ws connection. The UserManager context is disposed after the Hub connection is upgraded and initialized.

I would recommend using an OAuth Bearer Token approach. Rather than calling this.Context.User.Identity.Name in each SignalR method, obtain the UserName from the bearer token.

Microsoft documentation shows an example of this: Bearer token authentication

Related