Rest API Server is already setup an and authentication is tested. ASP Net JWT Authentication – Test
Now it’s time to setup Blazor Wasm fontend to support registration, login and Logout. Preferably in a appealing manner.
Let’s go
Within Client project add necessary packages
dotnet add package blazored.localStorage
dotnet add package Microsoft.AspNetCore.Components.Authorization
dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication
dotnet add package Microsoft.Extensions.Http
To enter account data forms are needed. As Blazor already uses Bootstrap, this style library can be used to make the forms appealing.
@using tryout_blazor_api.Shared.Auth
@using tryout_blazor_api.Client.Services
@inject IAuthenticationService authenticationService
@inject ILogger<Login> Logger
<EditForm Model="@loginForm" OnValidSubmit="@HandleLogin">
<DataAnnotationsValidator />
<div class="form-group">
<label for="name">Username</label>
<InputText id="name" class="form-control" @bind-Value="loginForm.Username" />
<ValidationMessage For="@(() => loginForm.Username)" />
</div>
<div class="form-group">
<label for="password">Password</label>
<InputText id="password" class="form-control" type="password" @bind-Value="loginForm.Password" />
<ValidationMessage For="@(() => loginForm.Password)" />
</div>
<button class="btn btn-primary">
Login
</button>
</EditForm>
@code {
private LoginModel loginForm = new();
private void HandleLogin()
{
Logger.LogInformation("Login as '{Name}'", loginForm.Username);
authenticationService.Login(loginForm);
}
}
@using tryout_blazor_api.Shared.Auth
@using tryout_blazor_api.Client.Services
@inject ILogger<Register> Logger
@inject IAuthenticationService authenticationService
<EditForm Model="@registerForm" OnValidSubmit="@HandleRegister">
<DataAnnotationsValidator />
<div class="form-group">
<label for="name">Username</label>
<InputText id="name" class="form-control" @bind-Value="registerForm.Username" />
<ValidationMessage For="@(() => registerForm.Username)" />
</div>
<div class="form-group">
<label for="email">Email</label>
<InputText id="email" class="form-control" type="email" @bind-Value="registerForm.Email" />
<ValidationMessage For="@(() => registerForm.Email)" />
</div>
<div class="form-group">
<label for="password">Password</label>
<InputText id="password" class="form-control" type="password" @bind-Value="registerForm.Password" />
<ValidationMessage For="@(() => registerForm.Password)" />
</div>
<button class="btn btn-primary">
Login
</button>
</EditForm>
@code {
private RegisterModel registerForm = new();
private void HandleRegister()
{
Logger.LogInformation("Registering as '{Name}' '{Mail}'", registerForm.Username, registerForm.Email);
authenticationService.Register(registerForm);
}
}
Cool thing about Blazor forms is, binding between form input fields and the data holding object is done behind the scenes. Within form Element the reference to dataholding object is passed as Model
.
Further reading on Boostrap forms can be found in it’s documentation: https://getbootstrap.com/docs/4.0/components/forms/
Even validation of entered data is done automaticly and Blazor offeres Elements (DataAnnotationsValidator
, ValidationMessage
) to display validation errors.
Without knowing anything about CSS and with more or less no code, these forms are really responsive and clean. And also use the same Models and validation that is used on server side.
The popping effect is achived by using a modal. More or less creating a floating box above the actual page. In an object and component oriented DRY manner, this wrapping modal is put into it’s own lazor Component.
<div class="modal @modalClass" tabindex="-1" role="dialog" style="display:@modalDisplay; overflow-y: auto;">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
@if(Title is not null) {
<h5 class="modal-title">@Title</h5>
}
<button type="button" class="close" data-dismiss="modal" aria-label="Close" @onclick="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
@if(Body is not null) {
@Body
}
</div>
<div class="modal-footer">
@if(Footer is not null) {
@Footer
}
</div>
</div>
</div>
</div>
@if (showBackdrop)
{
<div class="modal-backdrop fade show"></div>
}
@code {
[Parameter]
public RenderFragment? Title { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
private string modalDisplay = "none;";
private string modalClass = "";
private bool showBackdrop = false;
public void Open()
{
modalDisplay = "block;";
modalClass = "show";
showBackdrop = true;
}
public void Close()
{
modalDisplay = "none";
modalClass = "";
showBackdrop = false;
}
}
Blazor offers a neat way to write wrapper components or templates using RenderFragment
. https://docs.microsoft.com/en-us/aspnet/core/blazor/components/templated-components?view=aspnetcore-6.0
So this Modal Component makes wrapping components really easy in a few lines.
@using Microsoft.AspNetCore.Components.Authorization
@using tryout_blazor_api.Client.Shared.Auth
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<AuthorizeView>
<Authorized>
Hello, <UserName /> <LogoutButton />
</Authorized>
<NotAuthorized>
<button class="btn btn-primary" @onclick="() => RegisterModal?.Open()">Register</button>
<button class="btn btn-primary" @onclick="() => LoginModal?.Open()">Login</button>
<Modal @ref="RegisterModal">
<Title>Register</Title>
<Body>
<Register />
</Body>
</Modal>
<Modal @ref="LoginModal">
<Title>Login</Title>
<Body>
<Login />
</Body>
</Modal>
</NotAuthorized>
</AuthorizeView>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code {
private Modal? RegisterModal { get; set; }
private Modal? LoginModal { get; set; }
}
Here the form is put into modal and a button is created that opens it.
As well as already introducing the usage of AuthorizeView
(doc), that allows conditional check if client is authenticated while staying with HTML syntax.
Some easier components are Logout button and getting authenticated users name.
@using tryout_blazor_api.Shared.Auth
@using tryout_blazor_api.Client.Services
@inject IAuthenticationService authenticationService
<button class="btn btn-primary" @onclick="HandleLogout">
Logout
</button>
@code {
private void HandleLogout()
{
authenticationService.Logout();
}
}
@using Microsoft.AspNetCore.Components.Authorization;
<AuthorizeView>
@context.User.Identity.Name!
</AuthorizeView>
Here’s a component / page for debugging of clientside authentication information.
It’s listing all claims and information that is contained within the JWT as well as showing how AuthoriveView
can be further specialized for user rules, allowing showing links only for clients with a certain rule set.
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization;
@using System.Linq;
@inject AuthenticationStateProvider AuthenticationStateProvider
<h3>ClaimsPrincipal Data</h3>
<p>@_authMessage</p>
@if (_claims.Count() > 0)
{
<table class="table">
<tr>
<th scope="col"></th>
<th scope="col">Type</th>
<th scope="col">Value</th>
</tr>
@foreach (var claim in _claims)
{
<tr>
<th scope="row"></th>
<td>@claim.Type</td>
<td>@claim.Value</td>
</tr>
}
</table>
}
<ul class="list-group">
<AuthorizeView>
<li class="list-group-item">is authorized</li>
</AuthorizeView>
<AuthorizeView Roles="User">
<li class="list-group-item">is User</li>
</AuthorizeView>
<AuthorizeView Roles="SightAdder">
<li class="list-group-item">is SightAdder</li>
</AuthorizeView>
<AuthorizeView Roles="Admin">
<li class="list-group-item">is Admin</li>
</AuthorizeView>
</ul>
<p>@_userId</p>
@code {
private string? _authMessage;
private string? _userId;
private IEnumerable<Claim> _claims = Enumerable.Empty<Claim>();
protected override async Task OnParametersSetAsync()
{
await GetClaimsPrincipalData();
await base.OnParametersSetAsync();
}
private async Task GetClaimsPrincipalData()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
_claims = user.Claims;
_userId = $"User Id: {user.FindFirst(c => c.Type == "ID")?.Value}";
}
else
{
_authMessage = "The user is NOT authenticated.";
}
}
}