10

Using Azure SignalR (not .Net Core SignalR), Is Clients.Users.SendAsync supported to send message to specific user?

[Authorize]
public class ChatHub : Hub
{
    public async Task OnNewMessage(string userId, string message)
    {
        await Clients.Users(userId) 
             //Does Azure SignalR supports sending message to specific user,
             // the way .Net Core SignalR does it
            .SendAsync("OnNewMessage", userId, message);
    }
}

If yes, How Asp.Net Identity Claims Principal gets passed to Azure SignalR?

Edit: 18th June 2021

Responses below are not addressing the concerns. Sorry but I had already followed documentation for customizing UserIdBasedProvider etc. I want to stress out that -

This question is pertaining to how SignalR handles .Users(<*some-user-id*>).Send();

Ideally we do .Client(<connection id>).Send() to send message to specific user. But for this architecture we must store userId and connectionId ourselves in DB etc.

.User(userId).Send() works perfectly with .Net Core SignalR. So I believe SignalR SDK must have been keeping map of each connection Id and user Id in memory.

Does Azure SignalR internally keeps track of UserId, Connection Id mapping and sends the message?

Abhijeet
  • 13,562
  • 26
  • 94
  • 175
  • 1
    The term "broadcast" should not be used to refer to unicast messages, btw. – Dai Jun 09 '21 at 11:28
  • I think you need to inherit IPrincipal class here and from there you can access User's identity – Harkirat singh Jun 18 '21 at 10:29
  • I think my answer addresses your concern. It's like i said, Azure Signalr service receives JWT token (user id is in this token) and establishes persistent connection with the client. And yes, the service keeps mapping between user and connection in memory and database and automatically synchronizes the data between instances. So when you do `.Users(<*some-user-id*>).Send()` your webapp sends a request to Azure Signalr service which has the mapping between user and connection and the service sends information to a client. – Alexander Mokin Jun 18 '21 at 12:08
  • 1
    Side note: I highly recommend using Groups over connection IDs and identity, tends to work better in scaled scenarios. https://consultwithgriff.com/signalr-connection-ids/ – Kevin Griffin Jun 18 '21 at 13:28
  • @AlexanderMokin would you be having any references pls for in memory mapping of connection id and user id. Did you mean `in memory in SignalR Service` – Abhijeet Jun 19 '21 at 03:05
  • HubLifetimeManager is responsible for connection management in signalr library. There are several implementations [in memory](https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs#L19) and [redis](https://github.com/dotnet/aspnetcore/blob/98a8d7a2be94f04f65e3463ab897fde58a62edb1/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs#L24) (for multi server scenario) – Alexander Mokin Jun 19 '21 at 22:09
  • 1
    But if you use Azure SignalR service it uses [WebSocketsHubLifetimeManager](https://github.com/Azure/azure-signalr/blob/e0b2e8da208f67c725691cc46c07b31db594f6df/src/Microsoft.Azure.SignalR.Management/WebsocketsHubLifetimeManager.cs#L14) which simply offloads connection management to a Azure signalr service, which is guranteed to work in multi-server scenario. How Azure signalr service manages connection is up to them, but i would assume they use RedisHubLifetimeManager internally because it supports multi-server scenario – Alexander Mokin Jun 19 '21 at 22:13
  • Check out [this line](https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs#L286) to see how it works for in-memory case. So it basically keeps UserIdentifier as a connection property and then filters connections by this property – Alexander Mokin Jun 19 '21 at 22:22
  • @AlexanderMokin on a different side note, could you tell me secret how do you read and able to locate such internals on GitHub? – Abhijeet Jun 20 '21 at 12:20
  • @Abhijeet just find an entry point (i.e. class and method names) then download the library code and code-navigate with Resharper to dig deeper. Or if you don't want to download the code, github now also supports code navigation but it's not as good as Resharper's. Also sometimes i have to use dotpeek (if source code is not available) or check the callstack to find out what methods and classes are being used – Alexander Mokin Jun 20 '21 at 14:11

2 Answers2

1

Yes, if you'd like to implement serverless arhcitecture you should use Azure Functions for that. The typical serverless architecture is going to look like this

azure signalr architecture

The client (i.e. browser) sends a negotiation request to an azure function, the response continas all information required to establish connection with Azure SignalR. You should attach JWT token to a negotiation request. How you obtain a JWT token is up to you. Attaching it is pretty simple just provide a token factory when you create a signalR connection

const connection = new signalR.HubConnectionBuilder()
    .withUrl('link to your azure functions api', {
        accessTokenFactory: () => {
            return 'your token'
        }
    })
    .build();

For this to work you also need to set up an upstream connection so that your Azure SignalR service can forward all events to your Azure Function. Azure funciton handles upstreamed events and sends requests to an Azure SignalR to broadcast data to given user or multiple users.

upstream set up

The code for the SendMessage method in a hub is very similar to regular hubs except for Azure funciton bindings. You'll also need to extend ServerlessHub class instead of Hub and to install Microsoft.Azure.WebJobs.Extensions.SignalRService package

public class ChatHub : ServerlessHub
{

    [FunctionName(nameof(SendToUser))]
    public async Task SendToUser([SignalRTrigger] InvocationContext invocationContext, string userName, string message)
    {
        await Clients.User(userName).SendAsync("new_message", new NewMessage(invocationContext, message));
    }

    [FunctionName("negotiate")]
    public SignalRConnectionInfo Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req)
    {
        var claims = GetClaims(req.Headers["Authorization"]);
        var userName = claims.First(x => x.Type == ClaimTypes.NameIdentifier);
        return Negotiate(userName.Value, claims);
    }


    private class NewMessage
    {
        public string ConnectionId { get; }
        public string Sender { get; }
        public string Text { get; }

        public NewMessage(InvocationContext invocationContext, string message)
        {
            Sender = string.IsNullOrEmpty(invocationContext.UserId) ? string.Empty : invocationContext.UserId;
            ConnectionId = invocationContext.ConnectionId;
            Text = message;
        }
    }
}

You can also find a code sample with step by step instructions here

Edit:

If you don't want to go completely serverless and want to use regular web app it's also perfectly fine. The architecture diagram would look like this

Web app architecture

And you should use regular hub class in this case. The code you posted should work just fine.

The connection management is done completely by Azure Signalr service, you don't need to do any additional synchronization and you don't need to store connection data in a database. Just set up the tier size and the number of units.

Here's the short description of how it works:

  1. Client (e.g. Js client in browser) sends request to a web app to a negotiate endpoint and receives link to azure signalr service and an access token. This is a JWT token. A JWT token consists of several parts. The second part of that token is the payload, which contains the claims.
  2. Using the data fetched from negotiate endpoint client connects to azure signalr service.

So the Azure SignalR service uses this access token, it receives from negotiate endpoint, to authenticate a user. You don't have to write any additional code for that it's all done under the hood by the library.

In case you use a web app, the negotiate request is handled by the SignalR Sdk, you don't need to write any addional code for that.

It's all described in this article

And you can find detailed guide about implementing authentication here

Alexander Mokin
  • 2,264
  • 1
  • 9
  • 14
  • thanks. did you mean to say it can not be done using regular web app with Asp.Net Identity? – Abhijeet Jun 16 '21 at 21:52
  • It's perfectly fine to use web app if you don't want to implement your app fully serverless. I updated the post with more details – Alexander Mokin Jun 16 '21 at 22:22
  • thanks were you able to answer 2nd part? `How Asp.Net Identity Claims Principal gets passed to Azure SignalR?` – Abhijeet Jun 16 '21 at 23:52
  • Negotiate request handler generates JWT access token which then passed to an Azure SignalR service. JWT token consists of several parts. The second part of that token is the payload, which contains the claims. – Alexander Mokin Jun 17 '21 at 00:06
  • 1
    @Abhijeet if you're curious how negotiation is implemented in the js client you can find code for that [here](https://github.com/dotnet/aspnetcore/blob/0f7dbbde63f71a5ea7b6f20d10a611177a1e8a00/src/SignalR/clients/ts/signalr/src/HttpConnection.ts#L245) and the code for negotiation endpoint [here](https://github.com/Azure/azure-signalr/blob/08f97cb966715a040e4d5df7c2ea022c27a1041a/src/Microsoft.Azure.SignalR.Management/Negotiation/NegotiateProcessor.cs#L28). If you'd like to find out more about JWT token structure check out [this link](https://jwt.io/introduction) – Alexander Mokin Jun 17 '21 at 22:41
  • I will mark your answer correct, need to validate few things before I do that. Is it ok? – Abhijeet Jun 19 '21 at 03:03
0

You can achieve this by Implementing your customized IUserIdProvider like this and register it as an singleton.

 public class UserIdProvider : IUserIdProvider
    {
        public string GetUserId(HubConnectionContext connection)
        {
            return connection.User.FindFirst(ClaimConstants.PreferredUserName).Value;
        }
    }

Related Reference and Github

Sajeetharan
  • 216,225
  • 63
  • 350
  • 396