.NET 8 Zero to Hero Series is LIVE! 🚀Join Now

14 min read

Custom Authentication in Blazor WebAssembly - Step-By-Step Detailed Guide

#dotnet #blazor

In this next part of the Blazor Blog Series, Let’s learn about implementing Custom Authentication in Blazor WebAssembly Project (Blazor.Learner). We will cover some core concepts that can get you familiar with how authentication works in Blazor Applications.

PS, This is quite a detailed Article with around 3000 words. So I suggest you to bookmark this page for future reference. Grab a drink and let’s get started!

Built-in Authentication

Microsoft Ships Blazor with Built-in Authentication that can get you started quickly. But this is not what we will be looking into today. The Built-in Approach has quite a lot of limitations. For instance, what if you already have a Blazor project and want to implement authentication? You would not be able to do so. However, here is how you can set up the out-of-the-box Authentication in Blazor WebAssembly.

authentication-in-blazor-webassembly

The other limitations of the built-in authentication in Blazor are as follows.

  1. You will always have to use the Entity Framework Core - Code First Approach. This is not ideal for most developers.

  2. This uses the old Razor Pages, not the new Razor components.

  3. If you already have a database with existing users, it might be really tough to integrate this authentication.

That is why we have Custom Authentication in Blazor. Let’s begin.

Implementing Custom Authentication in Blazor WebAssembly

We will start off from where we left off in our previous Part - Blazor CRUD with Entity Framework Core – Detailed Tutorial. You can get the source code here. (blazor-blog-series-part-3 branch)

PS, The provided GitHub link takes you to the repository branch where we left off. The master is the current branch.

Login & Register Models

As usual, we need to build the model classes that would take in various authentication parameters for login and registering new users. We will create these classes in the Shared Project under the Models folder. You will need to install a package to help us with the Data Annotations. (Validations for the Models)

Install-Package System.ComponentModel.Annotations

Let’s add the Register Model, Models/RegisterRequest.cs

public class RegisterRequest
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
[Compare(nameof(Password), ErrorMessage = "Passwords do not match!")]
public string PasswordConfirm { get; set; }
}

Now create a model for Login, Models/LoginRequest.cs

public class LoginRequest
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
public bool RememberMe { get; set; }
}

Finally, we will need one mode model in the Share Project that is going to hold the details of the current user. Models/CurrentUser.cs

public class CurrentUser
{
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
public Dictionary<string, string> Claims { get; set; }
}

That’s all to do in the Shared Project. Now we have classes to help hold authentication parameters. In this tutorial, we will go by using Microsoft Identity. You could switch this with your own logic if needed. Let’s a mode for our User. Since this Model will inherit from IdentityUser, we will be adding it to the Server Project. But before that, let’s add the required packages to our Server Project. PS, you may want to update all your existing packages before installing new ones.

Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson

Now in the Server Project, add a new class at Models/ApplicationUser.cs

public class ApplicationUser : IdentityUser
{
}

Let’s modify our ApplicationDbContext class to support identity. Navigate to Data/ApplicationDBContext.cs and make the following changes.

public class ApplicationDBContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options):base(options)
{
}
public DbSet<Developer> Developers { get; set; }
}

After that, navigate to Server/Startup.cs and make the following changes.

public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDBContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDBContext>();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.AddControllers().AddNewtonsoftJson();
services.AddControllersWithViews();
services.AddRazorPages();
}

We will be using Cookies for state management. You could use Session Storage as well. Do not forget to add Authentication and Authorization Middlewares to the application. Navigate to the Configure method of Startup.cs and add these lines below app.UseRouting();

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

Make sure that you add these lines in the same order. With that done, we can proceed to migrations and updating the database, so that we create the required Identity tabes.

add-migration Identity
update-database

The only thing that is left to do in Server Project is to actually add the API endpoint to log in, register, log out, and get current user details. Let’s add a new Controller for this at Controllers/AuthController.cs

[Route("api/[controller]/[action]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AuthController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
}

Line #1 - For accessing the endpoints like ..api/auth/login or …api/auth/register
Line #5-11 Constructor Injection of the Identity Interfaces for signing in and creating new users etc.

No, we will add the required Action Methods. Note that these are all very basic methods. You could probably implement your logic here if needed to match your business requirement.

Login - Controller Method

[HttpPost]
public async Task<IActionResult> Login(LoginRequest request)
{
var user = await _userManager.FindByNameAsync(request.UserName);
if (user == null) return BadRequest("User does not exist");
var singInResult = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!singInResult.Succeeded) return BadRequest("Invalid password");
await _signInManager.SignInAsync(user, request.RememberMe);
return Ok();
}

Register - Controller Method

[HttpPost]
public async Task<IActionResult> Register(RegisterRequest parameters)
{
var user = new ApplicationUser();
user.UserName = parameters.UserName;
var result = await _userManager.CreateAsync(user, parameters.Password);
if (!result.Succeeded) return BadRequest(result.Errors.FirstOrDefault()?.Description);
return await Login(new LoginRequest
{
UserName = parameters.UserName,
Password = parameters.Password
});
}

Here we are also returning a Login Model after a successful registration. This is to implement auto-login after the user registers.

Logout - Controller Method

[Authorize]
[HttpPost]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return Ok();
}

Get Current User - Controller Method

[HttpGet]
public CurrentUser CurrentUserInfo()
{
return new CurrentUser
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
Claims = User.Claims
.ToDictionary(c => c.Type, c => c.Value)
};
}

Returns the currently logged-in user with an authentication state. We will also fetch the claims that can possibly contain the Roles as well. With this, we can say that the coding needed in the Server Project is done. The only aim of the server project is to provide the data. We have built the Auth Endpoint that GETS or POSTS data from/to the Datasource.

Now, The Main Part - Authentication in Blazor WebAssembly

Firstly, there is one package that is needed to be installed on the Client Project. Let’s install it.

Install-package Microsoft.AspNetCore.Components.Authorization

As mentioned previously, we now have api endpoints to facilitate authentication stuff. Now the only task left for us is to utilize these endpoints in our Client Project, right? As usual, to access endpoints, let’s add a service and its interface. We will name them AuthService and IAuthService. So the idea is that we will access these api endpoints by using an instance of injected HTTP Client in the service class from our Client Project.

Add a new interface to the client project. Services/IAuthService.cs

public interface IAuthService
{
Task Login(LoginRequest loginRequest);
Task Register(RegisterRequest registerRequest);
Task Logout();
Task<CurrentUser> CurrentUserInfo();
}
```csharp
Let's add a concrete class and implement the previous interface. These implementations would just be using the injected HTTP client and calling the required api endpoints with POST / GET JSON methods.
```csharp
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
public AuthService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<CurrentUser> CurrentUserInfo()
{
var result = await _httpClient.GetFromJsonAsync<CurrentUser>("api/auth/currentuserinfo");
return result;
}
public async Task Login(LoginRequest loginRequest)
{
var result = await _httpClient.PostAsJsonAsync("api/auth/login", loginRequest);
if (result.StatusCode == System.Net.HttpStatusCode.BadRequest) throw new Exception(await result.Content.ReadAsStringAsync());
result.EnsureSuccessStatusCode();
}
public async Task Logout()
{
var result = await _httpClient.PostAsync("api/auth/logout", null);
result.EnsureSuccessStatusCode();
}
public async Task Register(RegisterRequest registerRequest)
{
var result = await _httpClient.PostAsJsonAsync("api/auth/register", registerRequest);
if (result.StatusCode == System.Net.HttpStatusCode.BadRequest) throw new Exception(await result.Content.ReadAsStringAsync());
result.EnsureSuccessStatusCode();
}
}

Authentication State Provider

As the name suggests, this class provides the state of authentication of the user in Blazor Applications. AuthenticationStateProvider is an abstract class in the Authorization namespace. Blazor uses this class which would be inherited and overridden by us with custom implementation for getting the user state. This state can be from either local storage, session storage, or cookies like in our case.

Let’s start by adding the Provider class in the Services folder. Let’s name it CustomStateProvider. As mentioned, this class will inherit the AuthenticationStateProvider class.

public class CustomStateProvider : AuthenticationStateProvider
{
private readonly IAuthService api;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService api)
{
this.api = api;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
throw new NotImplementedException();
}
}

You can see that, by default we get to implement a function GetAuthenticationStateAsync. Now this function is quite important as Blazor calls this very often to check the state of authentication of the user in the application. Get the point?

Also, we will be injecting the AuthService and the CurrentUser Model into the constructor of this class. The idea behind this is that we will not be directly using the service instance in the view (Razor Components), rather we will inject the instance of CustomStateProvider into our views that would in turn access the services.

Here is the finished State Provider class. I will explain it in a bit.

public class CustomStateProvider : AuthenticationStateProvider
{
private readonly IAuthService api;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService api)
{
this.api = api;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
try
{
var userInfo = await GetCurrentUser();
if (userInfo.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex.ToString());
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
private async Task<CurrentUser> GetCurrentUser()
{
if (_currentUser != null && _currentUser.IsAuthenticated) return _currentUser;
_currentUser = await api.CurrentUserInfo();
return _currentUser;
}
public async Task Logout()
{
await api.Logout();
_currentUser = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Login(LoginRequest loginParameters)
{
await api.Login(loginParameters);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Register(RegisterRequest registerParameters)
{
await api.Register(registerParameters);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}

GetAuthenticationStateAsync - Get the current user from the service object. if the user is authenticated, we will add his/her claims to a list and create and Claims Identity. After this, we will return an Authentication State with the required data.

The other 3 Methods are pretty straightforward. We will just be calling the required service methods. But here is one extra thing that I would want to explain. Now with every log in, registration, and logout, technically there is a state change in the authentication. We need to let the entire application know that the user’s state has changed. Hence we use a Notify Method and pass the current authentication state by calling the GetAuthenticationStateAsync. Pretty logical, yeah?

Now, to activate these services and dependencies in the Client Project, we need to register them into the container yeah? For this, Navigate to the client project’s Program.cs and make the following additions.

public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomStateProvider>());
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}

That’s done too! Now let’s work on adding the Razor Components for the UI. Here we will have to add 2 Components, i.e., the Login and the Register component. We will be protecting the entire application by securing it. That means only an authenticated user will be allowed to view any data on the page. So, we will have a separate Layout Component for the Login/Register components. Let’s add this first.

Navigate to the Shared Folder with the Client project. Here is where you would ideally add any shared Razor Components. In our case, let’s add a new Razor Component and call it, AuthLayout.razor

@inherits LayoutComponentBase
<div class="main">
<div class="content px-4">
@Body
</div>
</div>

You can see that it is quite a basic html file with an inherited tag. We will not concentrate on CSS/HTML in this series. However, this layout component is going to act as a container that will hold the Login and Register component within itself. Feel free to do all the fancy html stuff here.

PS, If you are going to add in css styles, I suggest you to add these properties in the site.css file located at the wwwroot/css folder to keep things organized.

Login Component

Create a new Razor Component under Pages/Authentication/Login.razor

@page "/login"
@layout AuthLayout
@inject NavigationManager navigationManager
@inject CustomStateProvider authStateProvider
<h1 class="h2 font-weight-normal login-title">
Login
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="loginRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" @bind-Value="loginRequest.UserName" autofocus placeholder="Username" />
<ValidationMessage For="@(() => loginRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="loginRequest.Password" />
<ValidationMessage For="@(() => loginRequest.Password)" />
<div class="form-check m-3">
<InputCheckbox id="inputRememberMe" class="form-check-input" @bind-Value="@loginRequest.RememberMe" />
<label class="form-check-label" for="inputRememberMe">Remember Me</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<label class="text-danger">@error</label>
<NavLink href="register">
<h6 class="font-weight-normal text-center">Create account</h6>
</NavLink>
</EditForm>
@code{
LoginRequest loginRequest { get; set; } = new LoginRequest();
string error { get; set; }
async Task OnSubmit()
{
error = null;
try
{
await authStateProvider.Login(loginRequest);
navigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}

Line 1 - Page route - ../login
Line 2 - Set the Layout to the AuthLayout component
Line 4 Injects our CustomStateProvider instance.

Then we have a bunch of HTML that defines a form with username and password input.

From Line 35, We define the Model object (LoginRequest) and build the function OnSubmit that gets fired when you submit the form with valid data. Here we just use the custom state provider to access the service and ultimately the api to verify the login.

Register Component

Similarly, we create a register component too under Pages/Authentication/Register.razor

@page "/register"
@layout AuthLayout
@inject NavigationManager navigationManager
@inject CustomStateProvider authStateProvider
<h1 class="h2 font-weight-normal login-title">
Register
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="registerRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" placeholder="Username" autofocus @bind-Value="@registerRequest.UserName" />
<ValidationMessage For="@(() => registerRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="@registerRequest.Password" />
<ValidationMessage For="@(() => registerRequest.Password)" />
<label for="inputPasswordConfirm" class="sr-only">Password Confirmation</label>
<InputText type="password" id="inputPasswordConfirm" class="form-control" placeholder="Password Confirmation" @bind-Value="@registerRequest.PasswordConfirm" />
<ValidationMessage For="@(() => registerRequest.PasswordConfirm)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Create account</button>
<label class="text-danger">@error</label>
<NavLink href="login">
<h6 class="font-weight-normal text-center">Already have an account? Click here to login</h6>
</NavLink>
</EditForm>
@functions{
RegisterRequest registerRequest { get; set; } = new RegisterRequest();
string error { get; set; }
async Task OnSubmit()
{
error = null;
try
{
await authStateProvider.Register(registerRequest);
navigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}

You can see there are a bunch of unresolvable errors that probably say that a few items are not defined in the namespace or something. This is because we have not imported the new namespaces. To do this, navigate to _Import.razor and add these lines to the bottom.

@using Blazor.Learner.Client.Services
@using Microsoft.AspNetCore.Components.Authorization

Now, the error will go away. Now, we need to let Blazor know that we have authentication enabled and need to pass Authorize attribute throughout the application. For this, modify the Main Component, i.e., App.razor component.

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>

What is AuthorizeRoteView?
It’s a combination of AuthorizeView and RouteView, so that is displays the specific page but only to users who are authorized.

What is CascadingAuthenticationState?
This provides a cascading parameter to all descendant components.

Let’s add a logout button to the top navigation of the application to allow the user to log out. We will have to make changes to the MainLayout.razor component for this.

@inherits LayoutComponentBase
@inject NavigationManager navigationManager
@inject CustomStateProvider authStateProvider
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row">
<button type="button" class="btn btn-link ml-md-auto" @onclick="@LogoutClick">Logout</button>
</div>
<div class="content px-4">
@Body
</div>
</div>
@functions{
[CascadingParameter]
Task<AuthenticationState> AuthenticationState { get; set; }
protected override async Task OnParametersSetAsync()
{
if (!(await AuthenticationState).User.Identity.IsAuthenticated)
{
navigationManager.NavigateTo("/login");
}
}
async Task LogoutClick()
{
await authStateProvider.Logout();
navigationManager.NavigateTo("/login");
}
}

And finally, we will need to display our username on the Index Component soon after we log in. Navigate to Pages/Index.razor and make the following changes.

@page "/"
<AuthorizeView>
<Authorized>
<h1>Hello @context.User.Identity.Name !!</h1>
<p>Welcome to Blazor Learner.</p>
<SurveyPrompt Title="How is Blazor working for you?" />
</Authorized>
<Authorizing>
<h1>Loading ...</h1>
</Authorizing>
</AuthorizeView>

Let’s test our Application now.

authentication-in-blazor-webassembly

authentication-in-blazor-webassembly

authentication-in-blazor-webassembly

Summary

In this article, we have gone in-depth into Custom Authentication in Blazor WebAssembly. I will be updating this article with more data for you to learn. I hope this covers the entire Custom Authentication in Blazor WebAssembly. Let me know if I missed out on anything. You can find the completed source code here. Share this article within your developer community so that other developers can get a taste of Blazor! Happy Coding :D

Source Code ✌️
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❤️
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Supercharge your .NET Skills 🚀

I have started a .NET 8 Zero to Hero Series. We will cover everything from the basics to advanced topics to help you with your .NET Journey!

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy