We have already covered the authentication process for the Blazor WebAssembly standalone application communicating with ASP.NET Core Web API. Also, we’ve learned about Blazor WebAssembly and IdentityServer4 authentication. So, as a continuation of the Blazor WASM authentication, in this article, we are going to learn about Authentication in Blazor WebAssembly hosted applications. We are going to go over the authentication implementation of the server and client parts of the Blazor WebAssembly hosted app, and understand better how all the pieces fit into the big story.

To download the source code for this article, you can visit our Authentication in Blazor WebAssembly Hosted Applications repository

If you want to learn more about Blazor WebAssembly, we strongly suggest visiting our Blazor WebAssembly series of articles, where you can read about Blazor WebAssembly development, authentication, authorization, JSInterop, and other topics as well.

Let’s get going.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Project Creation with Default Authentication

Let’s start by creating a new Blazor WebAssembly application.

To create a hosted application, we have to check the ASP.NET Core hosted check box.

The default project doesn’t include authentication, so to include it, we have to choose Individual Accounts option:

Once we create our application, we are going to see three projects in our solution:

  • Shared – with a single model class
  • Client – that contains all the files for the Blazor WebAssembly client application
  • Server – which is a hosted server project for our client-side app

The Server and Client projects contain all the logic for the Authentication implementation, so let’s examine them step by step.

Server-Side Authentication in Blazor WebAssembly Hosted Applications

We’ve already covered the Identity implementation in the ASP.NET Core project, and that implementation is quite similar to what we currently have in our server-side project. Let’s open the Startup class and inspect the ConfigureServices method. In that method, we can find an Identity configuration:

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>();

Or if we are using .NET 6 and above, we have to open the Program class:

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false) 
 .AddEntityFrameworkStores<ApplicationDbContext>();

We can register Identity in the ASP.NET Core application with different methods, and here the AddDefaultIdentity method is used. This method configures commonly required Identity services with the addition of UI, token providers, and cookie authentication. We can see all of this in the source code implementation:

public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
    services.AddAuthentication(o =>
    {
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
        o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies(o => { });

    return services.AddIdentityCore<TUser>(o =>
    {
        o.Stores.MaxLengthForKeys = 128;
        configureOptions?.Invoke(o);
    })
        .AddDefaultUI()
        .AddDefaultTokenProviders();
}

Basically, many different methods for Identity registration have been encapsulated inside the AddDefaultIdentity<TUser> extension method. Also, with this method, we can force different SignIn, LockOut, Password, and other rules. Right now, we just set that we don’t require a confirmed account for sign-in action: options.SignIn.RequireConfirmedAccount = false.

What we can see is that this method uses the ApplicationUser class to register Identity users and the ApplicationDbContext class to register EF implementation of Identity stores. As we explained in our article, you can use the AplicationUser custom class, which inherits from IdentityUser, to customize user fields in Identity’s AspNetUsers table.

In the mentioned article, you can read that the ApplicationDbContext class inherits from IdentityDbContext<TUser> class. But here, this is not the case, at least there is no direct inheritance. The AplicationDbContext class inherits from the ApiAuthorizationDbContext class, which then inherits from the IdentityDbContext class:

public class ApiAuthorizationDbContext<TUser> : IdentityDbContext<TUser>, ...

The main reason for this is that the ApiAuthorizationDbContext class contains two DbSet properties needed for IdentityServer (PersistedGrands and DeviceFlowCodes).

Let’s Get Back to the Configuration Setup

Bellow the Identity registration, we can find an IdentityServer registration:

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

Or in .NET 6 and above:

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

With the AddIdentityServer method, we register the IdentityServer in our app. But we can see one additional extension method AddApiAuthorization<TUser, TContext>. This method configures IdentityServer for the ASP.NET Core purposes. In our IdentityServer 4 series of articles, we talked about IdentityServer registration with ASP.NET Core app and Web API app as well. Well, this method does all these different registrations for us. It registers an operational store, identity resources, API resources, and clients, so we don’t have to do it manually. If we inspect the source code, we can find a familiar code (if you are familiar with IdentityServer4 configuration):

public static IIdentityServerBuilder AddApiAuthorization<TUser, TContext>(this IIdentityServerBuilder builder, Action<ApiAuthorizationOptions> configure)
    where TUser : class
    where TContext : DbContext, IPersistedGrantDbContext
{
    if (configure == null)
    {
        throw new ArgumentNullException(nameof(configure));
    }

    builder.AddAspNetIdentity<TUser>()
        .AddOperationalStore<TContext>()
        .ConfigureReplacedServices()
        .AddIdentityResources()
        .AddApiResources()
        .AddClients()
        .AddSigningCredentials();

    builder.Services.Configure(configure);

    return builder;
}

This is a basic setup, but since it accepts an Action delegate parameter, we can use it to customize the IdentityServer configuration if we need to:

Authentication in Blazor WebAssembly Hosted - Configuration code

Authentication and JWT Registration

Finally, in the ConfigureServices method or in the Program class for .NET 6 and above, we can find a method that configures authentication services and a method that registers a handler for validating JWTs issued from IdentityServer:

services.AddAuthentication()
    .AddIdentityServerJwt();

If we have a .NET 6 and above project:

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

The AddIdentityServerJwt method registers a policy to handle all requests routed to any subpath in the Identity URL space /Identity. It also registers JwtBearer with its configuration and registers an API resource with a specific name composed of two parts – ApplicationName + API keyword (apiName keyword). We can see this in the source code:

public static class AuthenticationBuilderExtensions
{
    private const string IdentityServerJwtNameSuffix = "API";

    private static readonly PathString DefaultIdentityUIPathPrefix = new PathString("/Identity");

    public static AuthenticationBuilder AddIdentityServerJwt(this AuthenticationBuilder builder)
    {
        ...

        services.AddAuthentication(IdentityServerJwtConstants.IdentityServerJwtScheme)
            .AddPolicyScheme(IdentityServerJwtConstants.IdentityServerJwtScheme, null, options =>
            {
                options.ForwardDefaultSelector = new IdentityServerJwtPolicySchemeForwardSelector(
                    DefaultIdentityUIPathPrefix,
                    IdentityServerJwtConstants.IdentityServerJwtBearerScheme).SelectScheme;
            })
            .AddJwtBearer(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, null, o => { });

        return builder;

        IdentityServerJwtBearerOptionsConfiguration JwtBearerOptionsFactory(IServiceProvider sp)
        {
            var schemeName = IdentityServerJwtConstants.IdentityServerJwtBearerScheme;

            var localApiDescriptor = sp.GetRequiredService<IIdentityServerJwtDescriptor>();
            var hostingEnvironment = sp.GetRequiredService<IWebHostEnvironment>();
            var apiName = hostingEnvironment.ApplicationName + IdentityServerJwtNameSuffix;

            return new IdentityServerJwtBearerOptionsConfiguration(schemeName, apiName, localApiDescriptor);
        }
    }
}

Configure Method, Configuration Controller, and appSettings.json File

To finish with the configuration, we have to inspect the Configure method. There we can find three code lines important for the authentication, authorization, and IdentityServer:

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

This code is the same for .NET 6, just we have to inspect the pipeline part of the Program class.

And that’s it regarding the configuration.

Now, since the authentication process is strongly connected to the OIDC protocol, the application must have initial configuration parameters. In this case, these parameters are provided from the OidcConfiguration controller:

OIDC Configuration in Authentication in Blazor WebAssembly Hosted app

If you are familiar with the OIDC protocol, you will find these parameters quite familiar. If you are not, we strongly suggest reading our IdentityServer 4, OAuth, OIDC series, where you can read more about IdentityServer4 security with different client apps (MVC, Angular, Blazor WASM).

Finally, in the appsettings.json file, we can find a list of IdentityServer clients. In this case, we have only one client with the name Application's name.Client suffix:

"IdentityServer": {
   "Clients": {
     "BlazorWasmHostedAuth.Client": {
       "Profile": "IdentityServerSPA"
     }
   }
 },

In the same file, we can modify the connection string to point to our database:

"ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=BlazorWasmHostedDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Now, we can run the Update-Database command and inspect our created tables:

Migrated database

You can find all the migration files in the Data folder in the Server project.

Nice. We can move on to the Client project.

Client-Side Authentication in Blazor WebAssembly Hosted Applications

The first important part regarding the client-side authentication in Blazor WebAssembly hosted apps is Microsoft.AspNetCore.Components.WebAssembly.Authentication package. When using the authentication template, this package is already installed for us and referenced from the index.html file:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorWasmHostedAuth</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWasmHostedAuth.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

Then, there are several components that provide the authentication mechanism in the Blazor WebAssmebly application.

First, the App.razor component – as the central part of the BlazorWebAssembly authentication:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p>You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

We won’t go deep into explaining all of these components inside the file, for that, we have an article that explains all the components (CascadingAuthenticationState, AuthorizeRouteView…) in great detail. Basically, in this component, the AuthorizeRouteView component checks if the current user is authenticated. If that’s not the case, the user is redirected to the Login page with the RedirectToLogin component.

We can find the RedirectToLogin component in the Shared folder of the client project:

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
    }
}

This component uses NavigationManager service to navigate the user to the Authentication component with the login action as a parameter and additional query string for returnUrl.

We can find the Authentication component in the Pages folder:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

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

Once the user is navigated to this component with the required action, this component calls the RemoteAuthenticatorView component and passes the action as the parameter.

The RemoteAuthenticatorView component comes from the Microsoft.AspNetCore.Components.WebAssembly.Authentication package and it properly handles different actions at each stage of authentication.

Additional Components and Registration

Now, if you start the application, you are going to see the navigation menu with the Register and Log in links:

Navigation menu in Blazor WebAssembly Hosted app

These links come from the LoginDisplay component inside the Shared folder:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        <a href="authentication/profile">Hello, @context.User.Identity.Name!</a>
        <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");
    }
}

Here, we can see the Authorized component, which will be rendered if a user is authorized, and NotAuthorized component for unauthorized users. Inside that component, we can find two links with the href attributes pointing to the Authentication component with different actions.

Of course, for all of these to work, we must include the Microsoft.AspNetCore.Components.Authorization namespace. We can see this is the case if we open the _Imports.razor file.

Finally, we have the Program.cs class. In that class, we call the AddApiAuthorization method to add the authentication support for the Blazor client application:

builder.Services.AddApiAuthorization();

Also, the app configures the HttpClient to include access tokens when making requests to the API:

...

builder.Services.AddHttpClient("BlazorWasmHostedAuth.ServerAPI", 
    client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => 
    sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("BlazorWasmHostedAuth.ServerAPI"));
        
...

At this point, we can test our application.

Testing Authentication

Let’s start our app, and navigate to the Register page:

Registration page in the Blazor WebAssembly Hosted app

We can see that the URI points to /Identity/Account/Register page, which is handled by the policy registered with the AddIdentityServerJwt method. As a result of the successful registration action, the application will sign in the user and redirect them to the Home page:

Authenticated user.

Now let’s see how we can modify the authentication user interface as well as Identity pages.

Customizing Components and Identity Pages

Before we customize anything on the client side, let’s just click the Logout button in the menu:

 Logged out user interface

As a result, we are seeing the logged-out page with a simple message. The RemoteAuthenticatorView component enables this page for us, and if we want, we can add custom behavior to it.

The first thing we are going to do is to create a new CustomLoggedOut.razor component:

<h3>You are successfully logged out</h3>
<p>
    You can always <a href="/authentication/login">Log in</a> again.
</p>

Then, all we have to do is to modify the Authentication.razor component:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action">
    <LogOutSucceeded>
        <CustomLoggedOut /> 
    </LogOutSucceeded>
</RemoteAuthenticatorView>

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

The RemoteAuthenticatorView component provides us with different components that we can modify to change the UI in certain authentication stages. Here, we modify the LogOutSucceded component by calling the CustomLoggedOut component that is going to show us a different logout message with the link to the Login page:

Custom LoggedOut comonent

Next to the LogOutSucceeded component, we can find LoggingIn, CompletingLoggingIn, LogInFailed, LogOut, CompletingLogOut, LogOutFailed, UserProfile, and Registering components.

Of course, we can modify the Register and Login pages, but that’s something we have to do on the server-side app.

Modifying Identity Pages

We’ve seen how to modify the authentication UI on the client-side, but we can modify the UI on the server-side project as well. All the pages for Registration, Login, Password Reset, etc. come from ASP.NET Core Identity. We can inspect the Solution Explorer window to confirm that:

Identity part of the server project in Blazor WebAssembly Hosted application

Right now, we can see only the _LoginPartial.cshtml file but we’ll come to that pretty soon.

So, in the Identity configuration part of this article, we didn’t ask for an email confirmation to complete the login action:

options.SignIn.RequireConfirmedAccount = false

But if we start our application and navigate to the Login page, we will find the link for resending the confirmation email. Because we are not supporting that kind of logic in our app, let’s say that we want to remove that link from the page. Since we can’t see that page in the Identity folder, we have to include it in the project – to be 100% accurate, it is already in the project, we just can’t see it.

That said, let’s include the Login page by right-clicking on the server project and choosing the Add/New Scaffolded Item... option. Then in the Add New Scaffolded Item window, we are going to select the Identity option and click Add:

New Scaffolded Item Window Identity option

After a few seconds, a new window appears with the pages that we can include in our project. For now, we are going to include only the Login page, select the data context class, and click the Add button:

Scaffolding the Login page for ASP.NET Core Identity

Soon after that, we are going to see the file with the message stating the support for ASP.NET Core Identity was added to our project. Also, if we inspect the Solution Explorer window, we are going to find our new file there alongside the other required files:

Imported Login page from Identity

Login Page Modification

The entire Identity part is created using Razor pages so, for the Login page, we can find two files – Login.cshtml and Login.cshtml.cs. The .cshtml file contains the HTML markup with a C# code using Razor syntax, and the .cshtml.cs file contains the C# code for handling page events. We are going to modify only the Login.cshtml page. So, to do that, all we have to do is to remove the code part that shows the resend confirmation link on the page (44-46 code line):

<p>
    <a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>

Of course, we can make a lot of changes in both files if we want, but this is enough for us to see how we can modify the content of the Identity pages.

Now if we start our app and navigate to the Login page, we won’t be seeing that link anymore.

Feel free to inspect all the other files to understand better what each of them is used for.

One more thing

If we try to navigate to the FetchData page without authenticating first, we are going to be redirected to the Login page. That’s because the FetchData page is protected with the @attribute [Authorize] attribute. The same thing we can find in the server-side project in the WeatherForecastsController file.

Conclusion

That’s it for now.

In this article, we have learned

  • How to create Blazor WebAssembly Hosted app with implemented authentication
  • The way that authentication works for the server and client projects
  • How to modify the client and server authentication UI pages

In the next article, we are going to learn how to use roles with the Blazor WebAssembly Hosted authentication.

Until then.

Best regards.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!