Azure Functions, SignalR, and Authorization
If you are using SignalR in Azure Functions with Users and Groups, there’s quite a gap in the available documentation online. With this post, I’m hoping to help you get your Functions + SignalR working with Users and Groups.
Users and Groups
It’s important to note that “groups” are transient in SignalR; when your client reconnects, they have to join the Group again. But to do so, you need a user.
Here’s an example of a Function that shows how to add a User to a Group:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[FunctionName("JoinGroup")] public Task JoinGroup([HttpTrigger(AuthorizationLevel.Anonymous, "get")]HttpRequest req, [SignalR(HubName = "siteActivity")] IAsyncCollector signalRGroupActions) { return signalRGroupActions.AddAsync( new SignalRGroupAction { UserId = "test@dev.com", // THIS IS A STATIC VALUE...WHERE DO WE ACTUALLY GET IT? GroupName = "test.group", Action = GroupAction.Add }); } |
(You do not have to do this as a discrete Function; you can add this logic to some other Function as well where it would make sense to join the User to a Group like right after they log in or perform some specific action in your application.)
Note that in this example, I’ve simply used a static value. That’s fine; it’s not that important where you get the value from since it’s just a string. The Microsoft documentation shows how you could get this from an IClaimsPrincipal , but does a terrible job of describing how that gets injected (a story for another day).
Sending a Message to a Group
Sending a Message to a Group is equally straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[FunctionName("PingSignalR")] public Task PingSignalR([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route="PingSignalR/{message}")]HttpRequest request, [SignalR(HubName = "siteActivity")] IAsyncCollector<SignalRMessage> signalRMessages, string message, ILogger log) { return signalRMessages.AddAsync(new SignalRMessage { GroupName = "test.group", Target = "pingSignalR", Arguments = new object[] { message } }); } |
While I’ve hard coded the group name here, it’s easy enough to specify it as an input parameter to the function (lowercase “f”) or get it from the request myself. The same goes for the target. Like the earlier example, this does not have to be a discrete function; I could easily add this logic to some other function to send a real-time notification during execution. I’ve made it a standalone Function here for ease of testing. Imagine that I want to update connected clients in real time when a user uploads a document; I could add this logic directly to the code that handles the upload, for example.
Note that Target ; it’s the flag which determines the “event” or the “topic” that the client should respond on — we’ll see it in the next section when we hook up the client.
Connecting the Client
This is equally straightforward. This post isn’t an intro to how to work with the SignalR client (there are plenty online) so I’ve omitted most of the setup and details, but here is the key connection initialization point in JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// configure the connection. const connection = new SignalR.HubConnectionBuilder() .withUrl("https://my.domain/api") // Functions root domain .configureLogging(SignalR.LogLevel.Information) .build(); // setup the message listener connection.on('pingSignalR', this.handlePing); connection.on('otherTarget', this.handleOtherTarget); connection.onclose(() => console.log('disconnected')); // start the connection. connection.start() |
See how we can add multiple handlers based on the target name?
So…What’s the Problem?
If you’ve gotten this far, everything looks pretty straight forward, right? All of this is pretty much par for the course as far as the online documentation goes. But now let’s think about something: when we send the Message targeted to the Group, SignalR has to find the connections which are mapped to Users in the target Group and push the Message out to those connections. Remember up above I mentioned that “it’s just a string“.
How does SignalR know the User associated to a given connection?
The secret sauce is that this actually comes from the call to initialize the client — or at least it should. When you look at that last block of JavaScript, the call to withUrl will automatically append a /negotiate to the end of the URL. This then gets mapped to a Function on the server code which returns a caller-specific instance of SignalRConnectionInfo which has the User information embedded. The sample code can be found in the documentation:
1 2 3 4 5 6 7 8 9 10 |
[FunctionName("negotiate")] public static SignalRConnectionInfo Negotiate( [HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req, [SignalRConnectionInfo (HubName = "chat", UserId = "{headers.x-ms-client-principal-id}")] SignalRConnectionInfo connectionInfo) { // connectionInfo contains an access key token with a name identifier claim set to the authenticated user return connectionInfo; } |
The return object has an access token generated by our Function which will be handed off to the JavaScript client and used when connecting to the actual Azure hosted SignalR service. In this case, the Function is running locally on my development machine, but the connection information has a URL and token to connect to SignalR in the cloud.
Note the binding of the UserId property; it’s actually coming from the headers. This is the only place in the API that allows you to associate a user identifier to a connection.
It’s using a Function binding expression. If you are using App Service Authentication, I assume that this works perfectly as that is where the document redirects you. However, we are experimenting with the fantastic WebAuthn so we are handling the credentialing internally at the moment. The question is, then, how can we inject our own header user ID? It seems that if we can intercept the call in the Function processing pipeline, we can alter the headers as needed, right?
Take 1: IFunctionInvocationFilter
IFunctionInvocationFilter is a feature of the WebJobs host which allows injecting custom logic into the Function processing pipeline. It’s quite limited at the moment as there is only an entry point before the Function is executed and one after it has completed. There are several blog posts about it and a (not so) striking lack of official documentation. This post by Saillesh Pawar was an extremely helpful starting point.
So hypothetically, all we need to do is to implement the interface and then in the OnExecutingAsync override, inject the x-ms-client-principal-id (or any custom header).
However, if you try this, you’ll get the following error when triggering negotiate (note, I’m using a custom example header here called x-my-auth-header ):
1 2 3 |
System.Private.CoreLib: Exception while executing function: negotiate. Microsoft.Azure.WebJobs.Host: Exception binding parameter 'connectionInfo'. Microsoft.Azure.WebJobs.Host: Error while accessing 'x-my-auth-header': property doesn't exist. |
(I’ve posted an issue to the GitHub repo)
The issue, as I understand it, is that it would appear that the evaluation of the SignalRConnectionInfoAttribute occurs before the evaluation of OnExecutingAsync . The result is that the header is not available to the attribute, but is available in the Function implementation. So what to do?
Take 2…or 20: Imperative Bindings to the Rescue
After many rounds with the code and trying different mechanisms, I stumbled upon imperative bindings. What puzzled me was whether this would work or not as the SignalRConnectionInfoAttribute seemed like both an input and output binding; it wasn’t clear to me at all how this should work. We get the SignalRConnectionInfo injected into our function, yet we simply return it. The question is: could we use imperative bindings to return an instance after we specified the user ID from a value in the header (or some other mechanism)?
It turns out, that we can:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[FunctionName("negotiate")] public async Task<SignalRConnectionInfo> GetSignalRInfo( [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req, IBinder binder) { string userId = req.Headers["x-my-custom-header"]; // This value is here, but not available in the binding for the attribute SignalRConnectionInfoAttribute attribute = new SignalRConnectionInfoAttribute { HubName = "myHubName", UserId = userId }; // This style is an example of imperative attribute binding; the mechanism for declarative binding described below does not work // UserId = "{headers.x-my-custom-header}" https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-concept-serverless-development-config SignalRConnectionInfo connection = await binder.BindAsync<SignalRConnectionInfo>(attribute); return connection; } |
We create an instance of the attribute, invoke the binder, and get back the connection info that we need!
We can either combine the IFunctionInvocationFilter with this to do some pre-processing on the request to place the header into the pipeline or we can add our own custom logic here to load and verify the user ID. If we want to use headers with JavaScript, we will need to use the filter because the JavaScript client does not offer an entry point to inject the header. We could conceivably also do this with the JavaScript client by setting the header directly on the XHR, but the right way is to generate a JWT for the client during authentication and then pass the JWT back as part of the SignalR connection setup. Then in the filter, validate the JWT and extract the user information from that.
If you’re doing custom token validation in Functions, you’re probably going to need this.
Thanks for this article. The MSFT documentation is lacking, to say the least.
Great stuff! Just what I needed! Given you a follow on Twitter