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.
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.
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 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.