Secure Serverless Real-Time Applications Using Microsoft Azure
Modern web applications usually contain some real-time capabilities.
As users, we're used to being notified when something of interest happens, like when we receive a new message or when another user adds new content.
Nowadays, it's pretty easy to add real-time features to an ASP.NET Core application if you use SignalR, which is baked into the core framework.
But scaling a real-time application can be challenging as it requires a 'backplane' like a Redis cache or SQL Server. The backplane is responsible for transporting and delivering messages in a load balanced environment and needs to be configured and managed carefully.
Fortunately, Microsoft Azure provides a managed, scalable and highly-available SignalR host that's comparable in price to that of using one of the traditional scaling strategies. The Azure SignalR service will handle scaling and load balancing for you. You can then focus on the business logic of your application and not have to worry about infrastructure.
One of the benefits of using the hosted SignalR service is that it integrates nicely with many Azure serverless services. Probably the core benefit of serverless computing is its consumption (or pay-as-you-go) pricing model. Effectively, this means you only pay for the compute time that your services actually use.
Compare this with paying to keep a dedicated server up and running, even when it's idle. Unless you're expecting consistent, high-volume usage, serverless is the way to go.
In May 2019, Microsoft announced the general availability of the API Management consumption tier. This was a welcome announcement because API Management can be one of the most expensive services to run on Azure.
When you combine the consumption tier API Management with a consumption tier Function App you can build highly-scalable and cost-effective serverless, microservices architectures.
One useful capability of the API Management service is it's ability to authenticate requests before they're routed to a backend service. For example, it can validate a JWT token issued by an authority like an Azure AD B2C directory.
If you lock down direct access to the backend service (by requiring an api key or through IP whitelisting) and instead require all client requests to go through the API Management gateway, then you've got a reasonably secure, serverless, microservices architecture.
This scenario is exactly what is shown in the diagram above.
Application architecture walk-through
Let's take a closer look at how this works.
We have a client, a single page application, that is hosted in an Azure blob storage account. The user of this app signs in to an Azure AD B2C directory which issues an OAuth2 access token in the form of a JWT.
The JWT token is used as a bearer token in all requests to the API Management gateway. The gateway has been configured to validate the JWT token so it forwards requests to backend services for authenticated clients only.
The first thing the client does is to try to establish a web socket connection with the SignalR service. It does this by issuing a negotiate request. Only authenticated clients can do this so anonymous clients won't be allowed to connect to the SignalR service.
The negotiate request is handled by a special function that's set up in the Azure Function App. Let's call this function the NegotiateFunction. Once the web socket connection has been established, SignalR can notify the client as and when messages come in.
Imagine another client connects to the SignalR service using the same mechanism. It can send messages to the SignalR service by calling a dedicated function in the Function App. Let's call this function the MessagesFunction. Requests to the MessagesFunction go via the gateway so require a bearer token. Anonymous requests are disallowed.
The MessagesFunction has been bound to the Azure SignalR service so can talk to it directly. Effectively, messages are relayed from the Function App to the SignalR service. The SignalR service can then notify all of the connected clients with the message it received.
One of the limitations of the Azure SignalR service is that it currently doesn't support calling hub endpoints over the web socket connection. This is why messages need to be sent to a function over https.
Hopefully in the future, the Azure team will allow RPC calls over a web socket connection. This would probably mean creating a websocket function binding but that remains to be seen.
In the meantime, you can just create a function for each hub endpoint that you need to call and bind it directly to the SignalR service.