Single Sign-on user authentication on Blazor WebAssembly SignalR client

SignalR is an inbuilt ASP.NET Core library that makes it easy to create interactive real-time applications. It enables fast two-way messaging between the client and the server. Under the hood, it uses some complex logic to enable all of this. But its external APIs hide all this complexity, so using SignalR in your code is incredibly easy.

You can build a SignalR client by using any platform and language. But one of the client types supported by Microsoft is Blazor WebAssembly. This allows you to connect compiled code from inside your browser to a server that hosts SignalR. So your WebAssembly application will be able to receive any updates from the server in real time.

One of very important considerations that you will need to take into account while developing any application is making it secure. And Blazor WebAssembly application that communicates with the server via SignalR is not an exception. To ensure that only authorized users and clients can access it, your application needs to implement both authentication and authorization. Authentication is when users are forced to prove that they are who they say they are. Authorization is when users require specific privileges to access specific resources.

The role of OpenID Connect and OAuth in SSO

The most convenient way to apply authentication and authorization is by using Single Sign-On (SSO). This allows you to store all user credentials information in one place, so you won’t require separate logins for separate applications. Once you’ve logged into one application, you will be able to access any other application that is connected to the same SSO provider. Essentially, logging into one applications logs you into the whole ecosystem.

In the nutshell, SSO typically works as follows:

  1. When an unauthenticated user opens any application, the application redirects to the login page of the SSO provider.
  2. User is asked to provide the username, password, and any other additional information, such as time-sensitive multifactor authentication code.
  3. If the information has been entered correctly, the SSO provider redirect back to the original application and passes a token to it, which contains user’s data in encoded format.
  4. The encoded token contains sufficient information to about user’s access rights and contains some measures to prevent forgery.
  5. As long as the token hasn’t expired or the user hasn’t deliberately logged out, the next visit will not result in the redirection to the login page, but the access to the resource will be granted right away.
  6. The token can be passed to other applications within the ecosystem to prove that the user is authenticated.

Thankfully, there are some industry-standard practices of enabling SSO. One of the most popular ways of doing this involves using a combination of OpenID Connect and OAuth. OpenID Connect is an authentication protocol and OAuth is an authorization protocol. And in this article, you will learn how to use an SSO provider that uses both of these protocols to provide a secure communication channel between a SignalR server and its Blazor client.

We will not cover all SSO concepts in detail. However, if you are completely new to SSO, links to high-quality resources have been added to the text, so you can learn the basics in your own time.

Hypothetical scenarios

Let’s imagine that we have a web application that manages a number of Internet of Things (IoT) devices. To see the status of each of the device and to issue commands to them, there is a Blazor WebAssembly view. SignalR allows the users to receive the status of the devices in real time.

Obviously, since this view is accessible over the web, the last thing you want is to allow anyone to access it. You don’t want any random person to be able to issue any command to any device and read data from it. Therefore it’s vitally important that you will only allow authenticated users to access this view. Moreover, you probably don’t want just any authenticated user to perform any type of action on the IoT devices. Therefore it’s important that you include restrictions based on specific user privileges.

And this provides a brief overview of our scenario. The complete solution containing the code samples that are used in this article is available in this GitHub repo.

Setting up an SSO provider

There are different SSO providers you can choose from that support OpenID Connect and OAuth. Some of them are free and open source, while others are only available as paid-for commercial solutions. The examples include Keycloak, Okta, Auth0, IdentityServer, etc.. All of them are similar to each other. In any case, all of them use the standard authentication and authorization protocols.

So, regardless of what SSO provider you use, you need to set up the following:

  • At least one SSO client (either representing both Blazor client and the SignalR server app, or representing both apps together)
  • A URL endpoint that the app(s) can connect to to obtain the JWT token after a successful login.

Adding SSO components to the server

To enable SSO components on the server that cover both OpenID Connect and OAuth, you will need to add the following NuGet package to the application that hosts SignalR:

Microsoft.AspNetCore.Authentication.JwtBearer

Then, if you are using .NET 6 templates or newer, you will need to locate Program.cs file. If you are using older templates, the changes will need to be applied to the code inside the Startup.cs file. If you are using .NET 6 templates, you can add the following code anywhere before builder.Build():

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "oidc";
})
.AddJwtBearer(options =>
{
    options.Authority = "< replace this with the URL of SSO provider endpoint >";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false
    };
    options.RequireHttpsMetadata = false;
});

If you are using .NET 5 style templates, you will need to place this code into ConfigureServices method of the Startup class and replace builder.Services with services.

This configuration is standard across ASP.NET Core and isn’t just applicable to SignalR. You can find out more about it from the official documentation. Likewise, you can apply some additional authorization policies by using AddAuthorization method. This is something that you will need to use if you ever need to implement authorization logic that is more complex than by simply relying on the roles and user names.

The above code examples configure authentication and authorization. What you will need to do next is enable them both. And to do so, you can insert the following lines of code anywhere after builder.Build() call:

app.UseAuthentication();
app.UseAuthorization();

Or, if you are using .NET 5 templates, place this code inside Configure method of the Startup class.

Now, all you have to do to secure your SignalR endpoint is the same thing that you would have to do to secure any other endpoints in ASP.NET Core, whether it’s Web API controllers, gRPC service implementations or anything else. You just need to add AuthorizeAttribute to those methods on the SignalR Hub that you want to protect against unauthorized access. Or, if you want to protect all endpoint on the Hub, you just add it above the the definition of your Hub class.

And now we will move on to applying SSO protection to out Blazor SignalR client.

Enabling SSO on Blazor client

One of the easiest way to enable SSO on a Blazor WebAssembly client is by adding the following NuGet package to the project:

Microsoft.AspNetCore.Components.WebAssembly.Authentication

After downloading this NuGet package, we will need to add a reference to it into _Imports.razor file. And we will also need to add the following script reference to the index.html file, which you can find inside the wwwroot folder:

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

Then we can locate Program.cs file inside the Blazor client project and add the following code to it before builder.Buil():

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("oidc", options.ProviderOptions);
});

In this example, we are reading from appsettings.json file that should be placed into wwwroot folder. The content of this file should look similar to this:

{
  "oidc": {
    "Authority": "< insert the SSO provider URL here >",
    "ClientId": "< unique client id as specified in the SSO provider >",
    "ResponseType": "code",
    "DefaultScopes": [
      "openid",
      "profile"
    ],
    "PostLogoutRedirectUri": "authentication/logout-callback",
    "RedirectUri": "authentication/login-callback"
  }
}

One thing to note is that, for a Blazor application, you should configure your SSO provider to not require client secret. Client secret, as its name suggests, is meant to be kept private, so it’s OK to store it in the configuration of your server. But appsettings.json file in a Blazor WebAssembly client will be downloaded into the browsers of your users’ machines. It’s content will be visible to the users.

We can then add a bunch of shared views, which will force the login page to appear if the application is accessed by an unauthenticated user. All of these views will be already present in your application if you have selected “enable authentication” option when you have created your Blazor project. But these details would still be good to know to someone who is adding authentication capabilities to a project that previously didn’t have any.

We will place those components into the Shared folder. And the first component is represented by Authentication.razor file, which has the following content:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

This file passes the actions to the RemoteAuthenticatorView component, which handles connection with a remote SSO provider, as specified in the settings. The actions are represented by PostLogoutRedirectUri and RedirectUri entries from the settings. This is how the SSO provider will know which pages of the application to redirect to during login and log out.

Then we will add a new file to the same folder and call it LoginDisplay.razor. Its content will be as follows:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/register">Register</a>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

This is a component that will determine if the user is authenticated or not. If the user is authenticated, it will display a sign out button. If not, it will display navigational links for logging in and registering.

The next file we will add will be called RedirectToLogin.razor. It will have the following content, which will facilitate redirection to the login page for unauthenticated users:

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
    }
}

We can then enable this component by adding the following markup inside the Found component of the App.razor file that can be found in the root of the Blazor project:

<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p role="alert">You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>

Then we will apply some changes to the actual Blazor view that acts as a SignalR client. SignalR has its own authentication mechanism, so it won’t automatically recognize that the user is authenticated, even if the user has successfully logged in on the client. So, the first thing that we need to do is inject an object of a type of IAccessTokenProvider into the Razor component. You can either do it in C# class that’s attached to your Razor component, or you can do it in the component directly by using a statement similar to the following:

@inject IAccessTokenProvider TokenProvider

Once injected, we can extract the access token from this provider by executing RequestAccessToken() method, as shown in the example below:

var accessTokenResult = await TokenProvider.RequestAccessToken();

This will give us a response object that we can then extract the access token from by executing the following code:

string? accessToken = null;

if (accessTokenResult.TryGetToken(out var token))
{
    accessToken = token.Value;
}

Then, when we establish the connection with the SignalR, we can pass this token into the AccessTokenProvider property of SignalR connection options:

hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/devices"), options => {
                options.AccessTokenProvider = () => Task.FromResult(accessToken);
            })
            .Build();

And this is it. This is all you need to do to secure the communication between a Blazor client and SignalR server. Of course, we only had a look at a very basic example. But it covers all the fundamentals. It should be more than enough to get you started on securing SignalR applications.

Wrapping up

SSO authentication is one of several ways you can secure communication between SignalR server and its clients. This option is the most suitable for human users. However, other options might be better for other types of clients. For example, if your clients are IoT devices, it might be better to apply certificate authentication. And this is precisely what we will cover in the next article.

Also, if you want to learn everything about SignalR (including some uncommon use cases), I have written a book to help you with it. And to make it accessible to as many developers as possible, I made it significantly cheaper than what you would normally pay for a technical book of this kind. So, if you are interested, you can have a look at it via the link below:

SignalR on .NET 6 – The Complete Guide

All the best and happy coding!