Secure .NET 5 SignalR solutions with Azure AD
Tags: azure, dotnet, webdev
Published: 2021-06-11 21:53:04

If you haven't used .NET SignalR for real time communication or live data, then you're missing out. SignalR is a fantastic for anything thatn needs real time information and it uses a spoke-hub model for single, multi and broadcasting information. As with every solution, we also need to ensure that our communication channels are secure end to end. SignalR uses HTTPS in transit but anyone with the right URL can join. To protect against this and allow only certain clients or users to use the application, we can use Azure AD to protect our SignalR solution. And there's no better man to talk about SignalR than Brady Gaster, a principal PM in the .NET team. Brady joined us last week on the 425Show to build a secure SignalR solution using Azure AD.

You can catch the on demand video here:

{% youtube wZnHMa0V0kg %}

## Starting with a normal SignalR chat app

We have a barebones SignalR chat solution. Our back-end SignalR hub runs on an ASP.NET Core 5 web app and the front-end client is built using a console app. You can find the repo with the code on GitHub [here](https://github.com/425show/dotnet-chat)

All you have to do is clone, build and run. 
1. `git clone https://github.com/425show/dotnet-chat`
2. `cd chat.web`
3. `dotnet run`
4. On a new terminal `cd chat`
5. `dotnet run`
6. One the console app, connect and issue commands

You can see the solution running below!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4l9ixijyr9o09ihdxjon.gif)

## Let's create some Azure AD App Registrations

For our solution to be secured by Azure Active Directory, we need to create 2 Azure AD app registrations. To do this, we'll use an .NET Interactive Notebook. The Notebook is attached to the repo. You'll need to update the variables in each code segment and run it! A lot more straightforward than following a bunch of text-based instructions here!

Install the .NET Interactive extension in VS Code and run each code segment as per the instructions in the Notebook :)

This is a fun bit! I promise


## Adding authentication to our SignalR web App
Now that we have the AAD app registration details at hand, let's add the necessary code to secure, what is technically, an API. Our SignalR hub runs as an API. 

Open the terminal and type `dotnet add package Microsoft.Identity.Web`

Open the `appsettings.json` file and add the following section at the top

```
"AzureAd" : {
        "Instance" : "https://login.microsoftonline.com/",
        "Domain" : "<your tenant name>.onmicrosoft.com",
        "TenantId" : "<your tenant Id>",
        "ClientId" : "<your client Id>"
    },
```
The `TenantId` and `ClientId` will be available to you in the Notebook

Next, in `startup.cs` add the following at the top

```
using Microsoft.Identity.Web; 
```
and inside the `ConfigureServices()` method, add the following line at the end

```
services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
```

Finally, in the `Configure()` method, you need to add the following line of code right after the `app.UseAuthorization();`

```
app.UseAuthentication();
```
The last step to securing our SignalR Hub is to update the Hub class to only accept authenticated calls (ie. requests with a valid Access token) and with the right scope (i.e `user.chat`). We added a little bit of code to also retrieve the authenticated user's name so that we can display it in the logs - that's the icing on the cake

Update the `ChatHub.cs` with the following code:

```
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;

namespace chat.web.Hubs
{
    [Authorize]
    [RequiredScope("user.chat")]
    public class ChatHub : Hub
    {
        private readonly ILogger<ChatHub> logger;

        public ChatHub(ILogger<ChatHub> logger)
        {
            this.logger = logger;
        }
        
        public async Task SendMessage(string message)
        {
            await Clients.All.SendAsync("messageReceived", new {
                text = message,
                username = GetNameFromTokenClaims(this.Context)
            });
        }
        
        public override Task OnConnectedAsync()
        {
            var username = GetNameFromTokenClaims(this.Context);
            logger.LogInformation($"{username} just logged in and connected");
            return Task.CompletedTask;
        }

        private string GetNameFromTokenClaims(HubCallerContext context)
        {
            return context.User.Claims.FirstOrDefault(c => c.Type.Equals("Name", System.StringComparison.InvariantCultureIgnoreCase)).Value; 
        }
    }
}
```
Save and build the project to ensure that you have added everything as expected.

## Update the Console app to authenticate users
Now that the back-end (i.e the SignalR Hub) expects an access token to be passed with our requests, we need to update our client app to acquire a token on behalf of the user. 

Update the `Program.cs` with the following following code:

```
using System;
using System.Linq;
using System.Threading.Tasks;
using chat.Commands;
using Microsoft.Identity.Client;

namespace chat
{
    class Program
    {
        static string _accessToken = string.Empty;
        static string _redirectUri = "http://localhost";
        static string _clientId = "4eaa58bb-2d9a-40ea-ab65-d3103e3c2e68";
        static string _scope = "api://a15f51b4-acc6-4880-9213-64102e825a77/user.chat";

        static async Task Main(string[] args)
        {
            IPublicClientApplication app = PublicClientApplicationBuilder
                .Create(_clientId)
                .WithRedirectUri(_redirectUri)
                .WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
                .Build();

            AuthenticationResult result;
            var accounts = await app.GetAccountsAsync();
            var scopes = new string[] { _scope };

            try
            { 
                result = await app.AcquireTokenSilent(scopes, 
                    accounts.FirstOrDefault()).ExecuteAsync(); 
            }
            catch (MsalUiRequiredException) 
            { 
                result = await app.AcquireTokenInteractive(scopes).ExecuteAsync(); 
            }

            if(result != null)
            {
                _accessToken = result.AccessToken;
            }

            while (true)
            {
                Console.WriteLine("Enter Command:");

                var input = Console.ReadLine();
                HandleInput(input);
            }
        }


        static void HandleInput(string input)
        {
            if (input.StartsWith("connect", StringComparison.OrdinalIgnoreCase))
            {
                ConnectCommand.HandleCommand(input, _accessToken).Wait();
            }
            if (input.StartsWith("receive", StringComparison.OrdinalIgnoreCase))
            {
                ReceiveCommand.HandleCommand(input);
            }
            if (input.StartsWith("say", StringComparison.OrdinalIgnoreCase))
            {
                SayCommand.HandleCommand(input);
            }
        }
    }
}
```
This code instantiates a new MSAL PublicClient (look Ma! no secrets) and prompts the user to authenticate in order to get an access token to call the SignalR hub. 

We also have to update the `ChatClient.cs` file to make use of the access token Update the code as per the below:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;

namespace chat
{
    public class ChatClient
    {
        string _hubUrl = "https://localhost:5001/hubs/chat";
        private HubConnection _connection;

        public static ChatClient Instance { get; private set; }

        static ChatClient()
        {
            Instance = new ChatClient();
        }
        
        public async Task Connect(string accessToken)
        {
            _connection = new HubConnectionBuilder()
                .WithUrl(_hubUrl, options => {
                    options.AccessTokenProvider = () => Task.FromResult(accessToken);
                })
                .Build();

            await _connection.StartAsync();
        }

        public async Task Disconnect()
        {
            if(_connection != null && _connection.State == HubConnectionState.Connected)
            await _connection.DisposeAsync();
        }
    }
}
```
Finally, we need to update the `Connect.cs` file to pass the token to the `ChatClient` as per the code below:

```
using System;
using System.Threading.Tasks;

namespace chat.Commands
{
    public class ConnectCommand
    {
        public static async Task HandleCommand(string command, string accessToken)
        {
            Console.WriteLine("Starting SignalR Connection");
            await ChatClient.Instance.Connect(accessToken);
        }
    }
}
```
Save and build!

We are now ready to put our code to the test. Run the `chat.web` and then fire up the console app. The first thing you'll notice is that the console app prompts you to authenticate and consent to the permissions. One of these permissions is the `User.Chat` that we have defined in our Azure AD app registration.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enynf0k0wqaz8m06uwbi.png)

If everything has been configured correctly, and why wouldn't it - the Notebook did it's magic, you should be presented with this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yue57b3el0fmsask8l7m.png)

And with authentication working end-to-end this is the full experience

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1g16e1e4rkkrxvppasiv.gif)

With these few code changes we were able to secure our SignalR solution. 

## Source Code
You can find the fully working secure solution on our [GitHub repo](https://github.com/425show/dotnet-chat/tree/aad-auth)

I hope that this helps you on your project as well, and as always, make sure to let us know in the comments if you have any questions.