Because Developers are Awesome

Azure Active Directory Authentication for Blazor WASM (Part 4: SignalR)

  • aspnetcore
  • blazor
  • azure_active_directory
  • microsoft_entra
  • signalr

I really thought I was through with Azure Active Directory! Honest! However, another key piece came up. My CloudMud is a real-time pub/sub application that uses SignalR. How do I authenticate to the SignalR connection? Well, like most things, there is a recipe. So let’s get to it.

The Server side of things

Let’s take a look at the unauthenticated version first. I need to integrate SignalR into my app. Fortunately, there is a tutorial for that. I just followed it. This involved:

  1. Adding the SignalR NuGet packages to the server project.

  2. Adding services to the Program.cs:

    builder.Services.AddSignalR();
    builder.Services.AddResponseCompression(options => \{
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] \{ "application/octet-stream" \});
    \});
3. Mapping a "hub" in the application builder:
```csharp
app.UseResponseCompression();
app.MapHub<GameHub>("/hub");
  1. Adding a basic hub:

    using Microsoft.AspNetCore.SignalR;
    namespace cloudmud.Server.Hubs
    \{
    public class GameHub : Hub
    \{
    public string Username
    \{
    get => "-";
    \}
    public async Task SendMessage(string message)
    \{
    await Clients.All.SendAsync("ReceiveMessage", Username, message);
    \}
    \}
    \}
I'm expecting that the authentication will provide the username, so it's just a dummy value for now.
## The Client side of things
After adding the SignalR NuGet packages to the project, all the code is in the `Pages\Index.razor` file:
```csharp
@page "/"
@using Microsoft.AspNetCore.SignalR.Client;
@attribute [AllowAnonymous]
@inject NavigationManager NavigationManager
@implements IAsyncDisposable
<h1>Hello, world!</h1>
<AuthorizeView>
<NotAuthorized>
<p>You are not authorized. Data will not be loaded.</p>
</NotAuthorized>
<Authorized>
<div class="form-group">
<label>Message: <input @bind="messageInput" size="50"/></label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>
<hr/>
<ul id="messagedList">
@foreach (var message in messages) {
<li>@message</li>
}
</ul>
</Authorized>
</AuthorizeView>
@code {
private HubConnection? hub;
private List<string> messages = new List<string>();
private string? messageInput;
protected override async Task OnInitializedAsync()
{
hub = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/hub"))
.Build();
hub.On<string, string>("ReceiveMessage", (user, message) =>
{
messages.Add($"{user}: {message}");
StateHasChanged();
});
await hub.StartAsync();
}
private async Task Send()
{
if (hub is not null)
{
await hub.SendAsync("SendMessage", messageInput);
}
}
public bool IsConnected => hub?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hub is not null)
{
await hub.DisposeAsync();
}
}
}

You can actually run this - it will work, but without authentication. Now, let’s add some authentication to this!

The Blazor side of authentication

When you send authorization with a HTTP call, you place it in a header (called Authorization). I can get the authorization token from MSAL via an IAccessTokenProvider, which can be injected into the page. Add the following to the top of the Index.razor file:

@inject IAccessTokenProvider TokenProvider

I can now write a method that gets the access token. It’s a recipe:

private async Task<string?> GetAccessTokenAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult != null)
{
if (tokenResult.TryGetToken(out AccessToken accessToken)) {
return accessToken.Value;
} else {
Console.WriteLine("[GetAccessTokenAsync] No access token from tokenResult.TryGetToken()");
}
} else {
Console.WriteLine("[GetAccessTokenAsync] No token result from TokenProvider.RequestAccessToken()");
}
return null;
}

The only thing that remains is to send the access token with each request. This is done by adjusting the HubConnection to use an access token provider:

protected override async Task OnInitializedAsync()
{
hub = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/hub"), options =>
{
options.AccessTokenProvider = () => GetAccessTokenAsync();
})
.Build();
hub.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
StateHasChanged();
});
await hub.StartAsync();
}

If you run the app, you will see the authorization header being added to the request:

However, the server doesn’t know anything about the client authentication - it isn’t watching for it.

The server side of authentication

To fix the hub to recognize the authentication, just add an [Authorize] attribute to it.

Set a breakpoint at the Clients.All.SendAsync() call, and inspect the context:

I want to call your attention to a couple of things:

  1. You can’t use Context.User.Name - it’s not filled in.
  2. You can use Context.User.Identity.Claims to get a real name.
  3. You can use Context.UserIdentity as a unique and stable ID for the user.

In my game, I’m going to do a database lookup on the Context.UserIdentity to get the “name” of the character being played. In this example, though, I’m going to use the “Name” claim for the username:

private string? Username
{
get => Context.User?.Claims?.FirstOrDefault(c => c.Type.Equals("name", StringComparison.OrdinalIgnoreCase))?.Value ?? "-";
}

If you try out the app now, you will see that your messages are echoed back to you with your name. If another user logs in (to a different web browser), you will see their messages appear with their name as well. Real-time pub/sub with authentication achieved!

Comments