14

Question: How do I manage anonymous users so that multiple tabs in a single browser are all updated when the Hub sends out a response?

The scenario is as follows:

I would like to integrate SignalR into a project so that anonymous users can live chat with operators. Obviously user's that have authenticated with iIdentity are mapped via the Client.User(username) command. But currently say an anonymous user is browsing site.com/tools and site.com/notTools I can not send messages to all tabs with only a connectionID. Only one tab gathers the response.

I have tried using IWC patch but that tool doesn't account for saving chat information into a database and I think passing variables via ajax isn't a secure way to read/write to a database.

I have also looked in the following: Managing SignalR connections for Anonymous user

However creating a base such as that and using sessions seems less secure than Owin.

I had the idea to use client.user() by creating a false account each time a user connects and delete it when they disconnect. But I would rather not fill my aspnetusers db context with garbage accounts it seems unnecessary.

Is it possible to use UserHandler.ConnectedIds.Add(Context.ConnectionId); to also map a fake username? Im at a loss.

Would it make sense to use iUserId provider?

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

Then create a database that stores IP addresses to connectionIDs and single usernames?

database:

Users: With FK to connection IDs

|userid|username|IP       |connectionIDs|
|   1  | user1  |127.0.0.1|  1          |
|   2  | user2  |127.0.0.2|  2          |

connectionIDs:

|connectionID|SignalRID|connectionIDs|
|      1     | xx.x.x.x|     1       |
|      2     | xx.xxx.x|     2       |
|      3     | xx.xxxxx|     2       |
|      4     | xxxxx.xx|     2       |

Then Possibly write logic around the connection?

public override Task OnConnected()
    {
     if(Context.Identity.UserName != null){
      //add a connection based on the currently logged in user. 
     }
    else{
     ///save new user to database?
     } 
  }

but the question still remains how would I handle multiple tabs with that when sending a command on the websocket?

update To be clear, my intent is to create a live chat/support tool that allows for in browser anonymous access at all times.

The client wants something similar to http://www.livechatinc.com

I have already created a javascript plugin that sends and receives from the hub, regardless of what domain it is on. (my client has multisites) the missing piece to the puzzle is the simplest, managing the anonymous users to allow for multi-tabbed conversations.

Community
  • 1
  • 1
Chris
  • 470
  • 1
  • 4
  • 15
  • What do you want is an anonymous user to be able to chat to the same site (ie site.com) from any open browser tab, right? – tede24 Jan 26 '16 at 23:21
  • @tede24 yes that's what I would like. – Chris Jan 27 '16 at 03:01
  • 3
    Can't you just set a cookie with some kind of SessionId (Guid), create a group for that Id and subscribe all connectionids there? That way you could have messages broadcasted to all tabs – tede24 Jan 27 '16 at 03:06
  • So what happened with this? Have you tried the solution or found any other? – tede24 Jan 29 '16 at 11:45

2 Answers2

6

I'm not following the false user account idea (don't know if it works), but will develop an alternative.

The goal could be achieved through a ChatSessionId cookie shared by all browser tabs and creating Groups named as this ChatSessionId.

I took basic chat tutorial from asp.net/signalr and added functionality to allow chat from multiple tabs as the same user.

1) Assign a Chat Session Id in "Chat" action to identify the user, as we don't have user credential:

    public ActionResult Chat()
    {
        ChatSessionHelper.SetChatSessionCookie();
        return View();
    }

2) Subscribe to chat session when enter the chat page

client side

    $.connection.hub.start()
        .then(function(){chat.server.joinChatSession();})
        .done(function() {
           ...

server side (hub)

public Task JoinChatSession()
{
    //get user SessionId to allow use multiple tabs
    var sessionId = ChatSessionHelper.GetChatSessionId();
    if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");

    return Groups.Add(Context.ConnectionId, sessionId);
}

3) broadcast messages to user's chat session

public void Send(string message)
{
    //get user chat session id
    var sessionId = ChatSessionHelper.GetChatSessionId();
    if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");

    //now message will appear in all tabs
    Clients.Group(sessionId).addNewMessageToPage(message);
}

Finally, the (simple) ChatSessionHelper class

public static class ChatSessionHelper
{
    public static void SetChatSessionCookie()
    {
        var context = HttpContext.Current;
        HttpCookie cookie = context.Request.Cookies.Get("Session") ?? new HttpCookie("Session", GenerateChatSessionId());

        context.Response.Cookies.Add(cookie);
    }

    public static string GetChatSessionId()
    {
        return HttpContext.Current.Request.Cookies.Get("Session")?.Value;
    }

    public static string GenerateChatSessionId()
    {
        return Guid.NewGuid().ToString();
    }
}
tede24
  • 2,304
  • 11
  • 14
  • While your answer will work, it doesn't adhere to what I need, which is a secure-styled chatting system. Any person can falsify their cookies and access chat's that they weren't intended to use. I need a solution that will manage ids based on data that can't be falsified...easily. @tede24 – Chris Feb 01 '16 at 19:10
  • This solution will work for one domain, but does not take into account that two domains need to share the cookie. – Chris Feb 03 '16 at 04:49
  • 1
    @chris two domains is not stated in the question, the example is always site.com/xx. Anyway, you can share cookies whenever you need with proper cookie settings – tede24 Feb 03 '16 at 12:31
  • @chris regarding security, I can't understand how sending and ID from the client and using it as group (Dan's answer) could be more secure than generating a GUID from the server.. – tede24 Feb 03 '16 at 12:58
  • While Dans answer requires that the client assign the ID, I can use his code to form a design that works for me, by creating a hashing string that can be created by a WebAPI from my server, more so your code specifically requires that a helper class from a View within RAZOR return the cookie. both of your codes will work but require different use cases. His use case was closet to my need. Once I have a code that works for me specifically ill provide an answer to this question on my own. – Chris Feb 04 '16 at 04:31
3

a solution widely adopted is to make the user register with his some kind of id with connection back , on the onConnected.

 public override Task OnConnected()
        {
            Clients.Caller.Register();


            return base.OnConnected();
        }

and than the user returns with a call with some kind of your own id logic

from the clients Register Method

 public void Register(Guid userId)
    {
        s_ConnectionCache.Add(userId, Guid.Parse(Context.ConnectionId));
    }

and you keep the user ids in a static dictionary ( take care of the locks since you need it to be thread safe;

static readonly IConnectionCache s_ConnectionCache = new ConnectionsCache();

here

 public class ConnectionsCache :IConnectionCache
{
    private readonly Dictionary<Guid, UserConnections> m_UserConnections = new Dictionary<Guid, UserConnections>();
    private readonly Dictionary<Guid,Guid>  m_ConnectionsToUsersMapping = new Dictionary<Guid, Guid>();
    readonly object m_UserLock = new object();
    readonly object m_ConnectionLock = new object();
    #region Public


    public UserConnections this[Guid index] 
        => 
        m_UserConnections.ContainsKey(index)
        ?m_UserConnections[index]:new UserConnections();

    public void Add(Guid userId, Guid connectionId)
    {
        lock (m_UserLock)
        {
            if (m_UserConnections.ContainsKey(userId))
            {
                if (!m_UserConnections[userId].Contains(connectionId))
                {

                    m_UserConnections[userId].Add(connectionId);

                }

            }
            else
            {
                m_UserConnections.Add(userId, new UserConnections() {connectionId});
            }
        }


            lock (m_ConnectionLock)
            {
                if (m_ConnectionsToUsersMapping.ContainsKey(connectionId))
                {
                    m_ConnectionsToUsersMapping[connectionId] = userId;
                }
                else
                {
                        m_ConnectionsToUsersMapping.Add(connectionId, userId);
                }
            }

    }

    public void Remove(Guid connectionId)
    {
        lock (m_ConnectionLock)
        {
            if (!m_ConnectionsToUsersMapping.ContainsKey(connectionId))
            {
                return;
            }
            var userId = m_ConnectionsToUsersMapping[connectionId];
            m_ConnectionsToUsersMapping.Remove(connectionId);
            m_UserConnections[userId].Remove(connectionId);
        }



    }

a sample call to Register form an android app

 mChatHub.invoke("Register", PrefUtils.MY_USER_ID).get();

for JS it would be kind of the same

 chat.client.register = function () {
    chat.server.register(SOME_USER_ID);
}
Dan Kuida
  • 1,017
  • 10
  • 18
  • While your example would work in the interim it does not solve my immediate issue. The application already exists, however we can not handle concurrent-tabs. That is the only issue. We do not want to force users to register for access to our sites. When a user connects and STARTS a conversation data is gather, Name, Email, Description of Query. While connected to a "registered" conversation, other tabs that are not "registered", do not get the immediate chat conversation. – Chris Feb 02 '16 at 00:58
  • user has a nickname - here is your "grouping" token - the goal is not to make the user register, the goal is to group his connections together, same as you would reply with Clients.All(). you can easily get the relevant connections using dynamic clients = Clients.Clients(userConnections.Select(x => x.ToString()).ToList()); and than push the data only to those connections. I am using the same with android client, and I do not have any registration – Dan Kuida Feb 02 '16 at 10:07
  • I am marking your response as the answer. It works in the interim and I'll grow upon it at a later date. – Chris Feb 03 '16 at 04:48
  • will be also great to see if you came with better solution later – Dan Kuida Feb 03 '16 at 09:38