nzsai
nzsai

Reputation: 103

Managing SignalR connections for Anonymous user

I am using SignalR version 2.1.2 with ASP.Net MVC 5 & NServiceBus and have following requirement

There is a signup page (anonymous authentication) in which SignalR is used to send notifications. Every form submit will generate a new connection id which needs to be kept in a collection so that I can send response to the client. Context.User.Identity.Name is empty hence _connections.Add(name, Context.ConnectionId); cannot be used in OnConnected() hub event as given in this post

Similar problem exists in Login page.

If there is a possibility to control the ConnectionId then I could overcome this situation but it looks like new version of SignalR has got rid of connection factory.

I am using Redis cache so one option is to write my own connection management code to keep these connection ids in it.

Second option is to use Forms Authentication in such a way that a 'Anonymous Role' is assigned to these users which restricts the usage to anonymous views/controllers but gives a 'Name' to the user so that Context.User.Identity.Name is not empty. With this I can use built in SignalR mechanism to manage connection ids for me.

Upvotes: 4

Views: 2803

Answers (2)

nzsai
nzsai

Reputation: 103

This is what we did in BaseAnonymousController

public class BaseAnonymousController : Controller
{
    protected override void OnAuthentication(System.Web.Mvc.Filters.AuthenticationContext filterContext)
    {
        if (filterContext.Controller.GetType().Name == "AccountController" && filterContext.ActionDescriptor.ActionName == "login")
        {
            Guid result;
            if (!string.IsNullOrEmpty(SessionVariables.UserId) && Guid.TryParse(SessionVariables.UserId, out result))
            {
                //Already a anonymous user, so good to go.
            }
            else
            {
                //Seems to be a logged in a user. So, clear the session
                Session.Clear();
            }
        }

        //Perform a false authentication for anonymous users (signup, login, activation etc. views/actions) so that SignalR will have a user name to manage its connections
        if (!string.IsNullOrEmpty(SessionVariables.UserId))
        {
            filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(SessionVariables.UserId, "Anonymous"));
        }
        else
        {
            string userName = Guid.NewGuid().ToString();
            filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(userName, "Anonymous"));
            FormsAuthentication.SetAuthCookie(userName, false);
            SessionVariables.UserId = userName;
        }

        base.OnAuthentication(filterContext);
    }
}

and used this class as base class for all of anonymous controllers.

public class AccountController : BaseAnonymousController
{
    [AllowAnonymous]
    public ActionResult Signup()
    {
        //Your code
    }

    [AllowAnonymous]
    public ActionResult Login()
    {
        //Your code
    }

    [AllowAnonymous]
    public ActionResult ForgotPassword()
    {
        //Your code
    }

    [AllowAnonymous]
    public ActionResult ForgotUsername()
    {
        //Your code
    }
}

In the SignalR hub (nothing extraordinary than what is in SignalR documentation)

public override Task OnConnected()
    {
        SignalRConnectionStore.Add(Context.User.Identity.Name, Context.ConnectionId);

        return base.OnConnected();
    }

    public override Task OnReconnected()
    {
        string name = Context.User.Identity.Name;
        //Add the connection id if it is not in it 
        if (!SignalRConnectionStore.GetConnections(name).Contains(Context.ConnectionId))
        {
            SignalRConnectionStore.Add(name, Context.ConnectionId);
        }

        return base.OnReconnected();
    }

    public override Task OnDisconnected(bool stopCalled)
    {
        SignalRConnectionStore.Remove(Context.User.Identity.Name, Context.ConnectionId);

        return base.OnDisconnected(stopCalled);
    }

This works for both anonymous and authenticated users.

SignalRConnectionStore class and Interface

public interface ISignalRConnectionStore
{
    int Count { get; }
    void Add(string userName, string connectionId);
    IEnumerable<string> GetConnections(string userName);
    void Remove(string userName, string connectionId);
}

internal class SignalRConnectionStore : ISignalRConnectionStore
{
    private readonly Dictionary<string, HashSet<string>> _connections = new Dictionary<string, HashSet<string>>();

    public int Count
    {
        get
        {
            return _connections.Count;
        }
    }

    public void Add(string userName, string connectionId)
    {
        if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
        {
            lock (_connections)
            {
                HashSet<string> connections;
                if (!_connections.TryGetValue(userName, out connections))
                {
                    connections = new HashSet<string>();
                    _connections.Add(userName, connections);
                }

                lock (connections)
                {
                    connections.Add(connectionId);
                }
            }
        }
    }

    public IEnumerable<string> GetConnections(string userName)
    {
        if (!string.IsNullOrEmpty(userName))
        {
            HashSet<string> connections;
            if (_connections.TryGetValue(userName, out connections))
            {
                return connections;
            }
        }

        return Enumerable.Empty<string>();
    }

    public void Remove(string userName, string connectionId)
    {
        if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
        {
            lock (_connections)
            {
                HashSet<string> connections;
                if (!_connections.TryGetValue(userName, out connections))
                {
                    return;
                }

                lock (connections)
                {
                    connections.Remove(connectionId);

                    if (connections.Count == 0)
                    {
                        _connections.Remove(userName);
                    }
                }
            }
        }
    }
}

Declare a static variable of SignalRConnectionStore in Hub class as below.

public class ProvisioningHub : Hub
{
    private static ISignalRConnectionStore SignalRConnectionStore;

    public ProvisioningHub(ISignalRConnectionStore signalRConnectionStore)
        : base()
    {
        SignalRConnectionStore = signalRConnectionStore; //Injected using Windsor Castle
    }
}

Upvotes: 2

GeekzSG
GeekzSG

Reputation: 973

Use Forms Authentication, store a Federated Cookie and store the hub region in the cookie as well.. In SignalR jQuery code, use a jQuery plugin to read HTTP cookie and get the region name and subscribe to notifications.

Alternatively, in your .cshtml, render jQuery with region populated from your View Model.

Note: Use FormsAuthentication.SetAuthCookie as this will create HTTP Only cookie and will be sent in Ajax and non-Ajax calls.

Upvotes: 0

Related Questions