azuresignalrsignalr-hubazure-signalrazure-entra-id

How to get Azure Entra User Id from SignalR Hub?


I'm working on a kind of "notification observer" service which uses SignalR to collect connected users from an Angular application (this app uses Azure Entra as auth). This "observer" service would detect (by Service Bus or a call to the Hub) that a new notification has been created by other application for a specific group of user ids (list of Entra user ids, so GUIDs), and then it should be able to send this notification only to these specific users if they are connected to a Hub instance. enter image description here

Thing is, I don't really know if this is feasible, I know that you can configure SignalR to require authentication with Azure Entra, but I'm struggling to find information about if SignalR can distinguish between these connected users by their Azure Entra Id, something like:

foreach(var entraUserId in notification.Users)
  await this.hubContext.Client(entraUserId).SendAsync(message);

Or if I have to do something special, I know .Client() expects a connectionId which maps to a specific connection, but I have the feeling that there is a link that I don't know about that would be able to map the Entra User Ids to connectionIds.


EDIT: Also I've just been digging into Azure Function Apps to implement something similar with Azure SignalR and Azure Service Bus, would a similar functionality be possible using Azure SignalR? Meaning, being able to notify specific Entra Ids from the server side, once the Function App gets triggered by hooking it up to the Service Bus?


Solution

  • To get a connection by User Id (which related to Azure AD objectId), you have to create and maintain a mapping between users and connections yourself. You can do this by adding overrides to the OnConnected and OnDisconnected events, as follows:

    public class ChatHub : Hub
    {
        private readonly static Dictionary<string, string> _connections = new Dictionary<string, string>();
    
        public override Task OnConnectedAsync()
        {
            var userId = Context.UserIdentifier; // This should be objectId GUID
            var connectionId = Context.ConnectionId;
    
            // Add the connection ID and user ID to the mapping
            lock (_connections)
            {
                if (!_connections.ContainsKey(userId))
                {
                    _connections[userId] = connectionId;
                }
                else
                {
                    _connections[userId] = connectionId;
                }
            }
    
            return base.OnConnectedAsync();
        }
    
        public override Task OnDisconnectedAsync(Exception exception)
        {
            var userId = Context.UserIdentifier;
    
            // Remove the connection ID and user ID from the mapping
            lock (_connections)
            {
                if (_connections.ContainsKey(userId))
                {
                    _connections.Remove(userId);
                }
            }
    
            return base.OnDisconnectedAsync(exception);
        }
    
        public async Task SendNotificationToUser(string userId, string message)
        {
            // Retrieve the connection ID for the specified user
            if (_connections.TryGetValue(userId, out var connectionId))
            {
                // Send the notification to the specified user's connection
                await Clients.Client(connectionId).SendAsync("ReceiveNotification", message);
            }
            else
            {
                // Code to handle instances where user is not found.
                // Log or ignore, I guess.
            }
        }
    }