How to Implement a Login Form/Screen in Blazor WebAssembly

This example uses ASP.NET Core 3.1. It is compatible with .NET 5 and probably .NET 6.

This example requires using browser local storage to store user information which is implemented through dependency injection. The logic that handles the user state is implemented in the MainLayout.razor file. This is because there is no simple way to for child elements to communicate with other child elements making it difficult to create a "LoginComponent" that will update all the elements on the screen. What is happening here is the MainLayout is a cascading parameter which can be access from the child pages in order to check logged in status and draw their content based on this information.

LocalStorage.cs

Create a new C# class file in the client root directory and name it LocalStorage.cs, then enter this code:

// LocalStorage.cs
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace Utilities
{
	public interface ILocalStorage
	{
		/// <summary>
		/// Remove a key from browser local storage.
		/// </summary>
		/// <param name="key">The key previously used to save to local storage.</param>
		public Task RemoveAsync(string key);

		/// <summary>
		/// Save a string value to browser local storage.
		/// </summary>
		/// <param name="key">The key to use to save to and retrieve from local storage.</param>
		/// <param name="value">The string value to save to local storage.</param>
		public Task SaveStringAsync(string key, string value);

		/// <summary>
		/// Get a string value from browser local storage.
		/// </summary>
		/// <param name="key">The key previously used to save to local storage.</param>
		/// <returns>The string previously saved to local storage.</returns>
		public Task<string> GetStringAsync(string key);

		/// <summary>
		/// Save an array of string values to browser local storage.
		/// </summary>
		/// <param name="key">The key previously used to save to local storage.</param>
		/// <param name="values">The array of string values to save to local storage.</param>
		public Task SaveStringArrayAsync(string key, string[] values);

		/// <summary>
		/// Get an array of string values from browser local storage.
		/// </summary>
		/// <param name="key">The key previously used to save to local storage.</param>
		/// <returns>The array of string values previously saved to local storage.</returns>
		public Task<string[]> GetStringArrayAsync(string key);
	}

	public class LocalStorage : ILocalStorage
	{
		private readonly IJSRuntime jsruntime;
		public LocalStorage(IJSRuntime jSRuntime)
		{
			jsruntime = jSRuntime;
		}

		public async Task RemoveAsync(string key)
		{
			await jsruntime.InvokeVoidAsync("localStorage.removeItem", key).ConfigureAwait(false);
		}

		public async Task SaveStringAsync(string key, string value)
		{
			var compressedBytes = await Compressor.CompressBytesAsync(Encoding.UTF8.GetBytes(value));
			await jsruntime.InvokeVoidAsync("localStorage.setItem", key, Convert.ToBase64String(compressedBytes)).ConfigureAwait(false);
		}

		public async Task<string> GetStringAsync(string key)
		{
			var str = await jsruntime.InvokeAsync<string>("localStorage.getItem", key).ConfigureAwait(false);
			if (str == null)
				return null;
			var bytes = await Compressor.DecompressBytesAsync(Convert.FromBase64String(str));
			return Encoding.UTF8.GetString(bytes);
		}

		public async Task SaveStringArrayAsync(string key, string[] values)
		{
			await SaveStringAsync(key, values == null ? "" : string.Join('\0', values));
		}

		public async Task<string[]> GetStringArrayAsync(string key)
		{
			var data = await GetStringAsync(key);
			if (!string.IsNullOrEmpty(data))
				return data.Split('\0');
			return null;
		}
	}

	internal class Compressor
	{
		public static async Task<byte[]> CompressBytesAsync(byte[] bytes)
		{
			using (var outputStream = new MemoryStream())
			{
				using (var compressionStream = new GZipStream(outputStream, CompressionLevel.Optimal))
				{
					await compressionStream.WriteAsync(bytes, 0, bytes.Length);
				}
				return outputStream.ToArray();
			}
		}

		public static async Task<byte[]> DecompressBytesAsync(byte[] bytes)
		{
			using (var inputStream = new MemoryStream(bytes))
			{
				using (var outputStream = new MemoryStream())
				{
					using (var compressionStream = new GZipStream(inputStream, CompressionMode.Decompress))
					{
						await compressionStream.CopyToAsync(outputStream);
					}
					return outputStream.ToArray();
				}
			}
		}
	}
}

Program.cs

Modify the client-side Program.cs file to have this additional line of code:

// Blazor WASM (client-size) Program.cs file
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace LoginForm.Client
{
	public class Program
	{
		public static async Task Main(string[] args)
		{
			var builder = WebAssemblyHostBuilder.CreateDefault(args);

			// NOTE: BEGIN NEW CODE
			builder.Services.AddSingleton<Utilities.ILocalStorage, Utilities.LocalStorage>();
			// NOTE: END NEW CODE

			builder.RootComponents.Add<App>("app");

			builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

			await builder.Build().RunAsync();
		}
	}
}

MainLayout.razor

Modify MainLayout.razor as follows:

@inherits LayoutComponentBase

@* NOTE: BEGIN NEW CODE *@
@inject Utilities.ILocalStorage LocalStorage

<CascadingValue Value="this">
@* NOTE: END NEW CODE *@

	<div class="sidebar">
		<NavMenu />
	</div>

	<div class="main">
		<div class="top-row px-4">


			@* NOTE: BEGIN NEW CODE (REPLACED THE "About" LINK) *@
			<div style="width:70px;text-align:right;">
				@if (loggedIn)
				{
					<a class="btn btn-sm btn-primary text-white" title="Account" href="account"><span class="oi oi-key"></span></a>
					<button class="btn btn-sm btn-danger" title="Logout" @onclick="Logout"><span class="oi oi-account-logout"></span></button>
				}
				else
				{
					<button class="btn btn-sm btn-success" title="Login" @onclick="OpenLogin"><span class="oi oi-account-login"></span></button>
				}
			</div>
			@* NOTE: END NEW CODE *@


		</div>

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

@* NOTE: BEGIN NEW CODE *@
</CascadingValue>
@* NOTE: END NEW CODE *@


@* NOTE: BEGIN NEW CODE *@

<div class="modal-backdrop fade @(show ? "show" : "") @(display ? "d-block" : "d-none")"></div>

<div class="modal fade @(show ? "show" : "")  @(display ? "d-block" : "d-none")" tabindex="-1" role="dialog">
	<div class="modal-dialog" role="document">
		<div class="modal-content">
			<div class="modal-header">
				<h5 class="modal-title">Login Form</h5>
				<button type="button" class="close" data-dismiss="modal" aria-label="Close" @onclick="Close">
					<span aria-hidden="true">&times;</span>
				</button>
			</div>
			<div class="modal-body">
				<div class="mb-3">
					<label for="loginEmail" class="form-label">Email</label>
					<input type="email" class="form-control" id="loginEmail" placeholder="name@example.com" autocomplete="off" required @bind-value="user" />
				</div>
				<div class="mb-3">
					<label for="loginPassword" class="form-label">Password</label>
					<input type="password" class="form-control" id="loginPassword" required />
				</div>
			</div>
			<div class="modal-footer">
				<button type="button" class="btn btn-success" @onclick="Login">Login</button>
				<button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="Close">Close</button>
			</div>
		</div>
	</div>
</div>

@* NOTE: END NEW CODE *@


@* NOTE: BEGIN NEW CODE *@

@code {

	private bool show, display, loggedIn;
	private string user;

	public string GetUserName()
	{
		return loggedIn ? user : null;
	}

	public async Task OpenLogin()
	{
		display = true;
		await Task.Delay(100);
		show = true;
	}

	public async Task Logout()
	{
		user = null;
		loggedIn = false;
		await LocalStorage.RemoveAsync("user");
	}

	private async Task Close()
	{
		show = false;
		await Task.Delay(500);
		display = false;
	}

	private async Task Login()
	{
		if (!string.IsNullOrEmpty(user))
		{
			await Close();
			loggedIn = true;
			await LocalStorage.SaveStringAsync("user", user);
		}
	}

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		user = await LocalStorage.GetStringAsync("user");
		loggedIn = !string.IsNullOrEmpty(user);
	}
}

@* NOTE: END NEW CODE *@

Modify NavMenu.razor as follows:

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">LoginForm</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
	<ul class="nav flex-column">
		<li class="nav-item px-3">
			<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
				<span class="oi oi-home" aria-hidden="true"></span> Home
			</NavLink>
		</li>
		<li class="nav-item px-3">
			<NavLink class="nav-link" href="counter">
				<span class="oi oi-plus" aria-hidden="true"></span> Counter
			</NavLink>
		</li>
		<li class="nav-item px-3">
			<NavLink class="nav-link" href="fetchdata">
				<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
			</NavLink>
		</li>
		@* NOTE: BEGIN NEW CODE *@
		<li class="nav-item px-3">
			<NavLink class="nav-link" href="account">
				<span class="oi oi-key" aria-hidden="true"></span> Account
			</NavLink>
		</li>
		@* NOTE: END NEW CODE *@
	</ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Account.razor

Create a new Razor page named Account.razor and add this code:

@page "/account"

@if (!string.IsNullOrEmpty(mainLayout.GetUserName()))
{
	<h3 class="my-4">Your Account</h3>

	<p>@mainLayout.GetUserName()</p>

	<button type="button" class="btn btn-danger" @onclick="mainLayout.Logout">Logout</button>
}
else
{
	<h3 class="my-4">Please Login</h3>

	<button type="button" class="btn btn-success" @onclick="mainLayout.OpenLogin">Login</button>
}
@code {
	[CascadingParameter]
	public MainLayout mainLayout { get; set; }

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
	}
}

Run the Application

Run the application and log in with any user name to see how access is controlled.

Coding Video

https://youtu.be/UtYjWbGG3A0


Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site.
OK