Using Azure SignalR from an Azure function


Posted onĀ 

The Azure SignalR service was announced at Build 2018 with the promise of being able to use SignalR without having a dedicated server for it. I really enjoy writing code with Azure Functions, so I though I'd combine the two and have serverless SignalR.

All the documentation for Azure SignalR shows how to use it from an ASP.NET Core application. Which is nice, but not what I want. Thankfully the github page has some info on how it all works. In addition, I created an ASP.NET Core app and observed the network traffic to get even more insight.

Setting up the connection

A SignalR client first makes a POST request to /negotiate/{hubName}, where hubName is specified by the client. The client expects a JSON response that looks like this

{
    "url": "https://signalr-service-name.service.signalr.net:5001/client/?hub=hubName",
    "accessToken": "generated token",
    "availableTransports": []
}
  • url is the url that the client will use to open the SignalR connection with. This is the url of the SignalR service in Azure.
  • accessToken is used to authenticate with the Azure SignalR service
  • I'm not really sure what availableTransports is used for. It appears that it should be an empty array - nothing else works and this seems to be what the ASP.NET Core SignalR response contains

Creating the function

The first step is to create a function that will receive the negotiate request

[FunctionName("Negotiate")]
public static IActionResult Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "{hub}/negotiate")] HttpRequest req,
    // the hub to connect to
    string hub)
{
    // This will be filled in later
    return new OkResult();
}

In order for this function to have the expected url, you'll need to modify your host.json file to look like this

{
  "http": { "routePrefix": "" }
}

Creating an access token

In order for the client to successfully connect to the Azure SignalR service it needs to have a valid access token. This access token should be a JWT signed by your Azure SignalR Access Key.

I used this JWT package for dotnet to generate the JWT, but you can use any method you like as long as you can generate a valid JWT.

The code to generate the token looks like this

// parse the connection string to get the endpoint url and access key
var (endpoint, accessKey) = ConnectionStringHelpers.ParseSignalRConnectionString();
var token = new JwtBuilder()
    .WithAlgorithm(new HMACSHA256Algorithm())
    .WithSecret(accessKey)
    .AddClaim("exp", DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds())
    // the aud claim is important and MUST match the hub url that is returned to client
    .AddClaim("aud", $"{endpoint}:5001/client/?hub={hub}")
    .Build();

This can then be returned to the caller

return new OkObjectResult(new
{
    url = $"{endpoint}:5001/client/?hub={hub}",
    accessToken = token,
    availableTransports = Enumerable.Empty<string>()
});

The complete Negotiate function looks like this

[FunctionName("Negotiate")]
public static IActionResult Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "{hub}/negotiate")] HttpRequest req,
    // the hub to connect to
    string hub)
{
    // parse the connection string to get the endpoint url and access key
    var (endpoint, accessKey) = ConnectionStringHelpers.ParseSignalRConnectionString();
    var token = new JwtBuilder()
        .WithAlgorithm(new HMACSHA256Algorithm())
        .WithSecret(accessKey)
        .AddClaim("exp", DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds())
        // the aud claim is important and MUST match the hub url that is returned to client
        .AddClaim("aud", $"{endpoint}:5001/client/?hub={hub}")
        .Build();
    return new OkObjectResult(new
    {
        url = $"{endpoint}:5001/client/?hub={hub}",
        accessToken = token,
        availableTransports = Enumerable.Empty<string>()
    });
}

At this point the client should be able to connect to the Azure SignalR service.

Broadcasting data

There a number of REST methods exposed to send messages over SignalR. They are documented in this swagger document.

For any call to this API you need to generate a JWT to authenticate with. This is almost identical to the negotiate call but requires a different aud claim.

The code looks like this

var (endpoint, accessKey) = ConnectionStringHelpers.ParseSignalRConnectionString();
var token = new JwtBuilder()
    .WithAlgorithm(new HMACSHA256Algorithm())
    .WithSecret(accessKey)
    .AddClaim("exp", DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds())
    // the aud claim must match the endpoint you're calling with this token
    .AddClaim("aud", $"{endpoint}:5002/api/v1-preview/hub/{hub}")
    .Build();

This token is valid for the broadcast endpoint which will send a message to all users connected to the hub.

You can then use this token to send a broadcast message.

var target = "the target method name";
var arguments = new string[] { "any", "args", "to", "pass" };
var httpClient = new HttpClient
{
    BaseAddress = new Uri($"{endpoint}:5002")
};
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
await httpClient.PostAsJsonAsync($"/api/v1-preview/hub/{hub}", new
{
    target,
    arguments
});

Javascript Client

The frontend side of this is pretty simple. Grab the @aspnet/signalr package from NPM.

Then you can connect and subscribe to a method

const connection = new HubConnectionBuilder()
    .withUrl(host + '/notifications') // notifications is the hub name
    .build();
connection
    .start()
    .then(function() {
        console.log('connected');
    })
    .catch(function(error) {
        console.error(error.message);
    });

const listenFunc = (data) => {
    console.log(data);
};

// subscribe to a method
connection.on(method, listenFunc);

// and then unsubscribe later on
connection.off(method, listenFunc);

Wrapping up

At this point you should be able to send messages from a function app over SignalR and receive them in a frontend JavaScript web app.

Hopefully this will be useful if you want to get Azure SignalR working in a serverless environment

*[JWT]: JSON Web Token