PROWAREtech

articles » current » blazor » wasm » jwt-authentication-simple

Blazor: Json Web Token (JWT) Authentication Example - Simple

A simple example of adding JWT Bearer authentication to Blazor WebAssembly (WASM); with examples written in C#.

These examples deal with both the server-side and client-side implementation. They use either .NET 8/Visual Studio 2022, .NET 6/Visual Studio 2022 or .NET Core 3.1/Visual Studio 2019.

Develop this application on Linux!

Pay close attention to all the comments in the code particularly ones that begin with "NOTE:".

.NET 8 Example

This example uses the new .NET 8 Blazor Web App which is a hybrid of server-side and client-side (WebAssembly).

See this article to add JWT bearer authentication to a .NET 8 Minimal Web API, which also can be used by Blazor WASM.

Create a new Blazor Web App (which includes WebAssembly) called "BlazorExample" in this example. Configure it for HTTPS, "Auto" Interactive render mode, "Per page/component" Interactivity location and to Include sample pages.

.NET 8 Blazor Web App Options

The next step is to add the correct NuGet packages to the Server and Client projects.

  1. Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the server project. Pay attention to the version, 8.0.x in this case.
  2. Add the System.ComponentModel.Annotations NuGet package to the client project.

These are the Blazor Web App Server-side installed NuGet packages:

Installed Blazor Server NuGet Packages

These are the Blazor Web App Client-side installed NuGet packages:

Installed Blazor Client NuGet Packages

Modify the Client Project's Code

Create a WeatherForecast.cs file located in the root of the client project.

// WeatherForecast.cs

namespace BlazorExample.Client
{
	public class WeatherForecast
	{
		public DateTime Date { get; set; }

		public int TemperatureC { get; set; }

		public string Summary { get; set; }

		public string UserName { get; set; }

		public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
	}
}

Create an AuthModel.cs file located in the root of the client project.

// AuthModel.cs
using System.ComponentModel.DataAnnotations;

namespace BlazorExample.Client
{
	public class LoginResult
	{
		public string message { get; set; }
		public string email { get; set; }
		public string jwtBearer { get; set; }
		public bool success { get; set; }
	}
	public class LoginModel
	{
		[Required(ErrorMessage = "Email is required.")]
		[EmailAddress(ErrorMessage = "Email address is not valid.")]
		public string email { get; set; } // NOTE: email will be the username, too

		[Required(ErrorMessage = "Password is required.")]
		[DataType(DataType.Password)]
		public string password { get; set; }
	}
	public class RegModel : LoginModel
	{
		[Required(ErrorMessage = "Confirm password is required.")]
		[DataType(DataType.Password)]
		[Compare("password", ErrorMessage = "Password and confirm password do not match.")]
		public string confirmpwd { get; set; }
	}
}

Modify the Server Project's Code

The client project will be modified again later when adding the razor pages.

Here is the UserDatabase.cs file located in the server project. It is a crude example of a database for storing users. It is file-system based. At this point one should already be familiar with using the database of their choice. The decision not to use a database in this example is done to make the example focus purely on JWT. See this article for help using Microsoft's SQL Server database.

// UserDatabase.cs
using System.Security.Cryptography;
using System.Text;

namespace BlazorExample
{
	public class User
	{
		public string Email { get; }
		public User(string email)
		{
			Email = email;
		}
	}
	public interface IUserDatabase
	{
		Task<User> AuthenticateUser(string email, string password);
		Task<User> AddUser(string email, string password);
	}
	public class UserDatabase : IUserDatabase
	{
		private readonly IWebHostEnvironment env;
		public UserDatabase(IWebHostEnvironment env) => this.env = env;
		private static string CreateHash(string password)
		{
			var salt = "997eff51db1544c7a3c2ddeb2053f052";
			var h = new HMACSHA256(Encoding.UTF8.GetBytes(salt + password));
			byte[] data = h.ComputeHash(Encoding.UTF8.GetBytes(password));
			return System.Convert.ToBase64String(data);
		}
		public async Task<User> AuthenticateUser(string email, string password)
		{
			if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
				return null;
			var path = System.IO.Path.Combine(env.ContentRootPath, "Users");
			if (!System.IO.Directory.Exists(path))
				return null;
			path = System.IO.Path.Combine(path, email);
			if (!System.IO.File.Exists(path))
				return null;
			if (await System.IO.File.ReadAllTextAsync(path) != CreateHash(password))
				return null;
			return new User(email);
		}
		public async Task<User> AddUser(string email, string password)
		{
			try
			{
				if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
					return null;
				var path = System.IO.Path.Combine(env.ContentRootPath, "Users"); // NOTE: THIS WILL CREATE THE "USERS" FOLDER IN THE PROJECT'S FOLDER!!!
				if (!System.IO.Directory.Exists(path))
					System.IO.Directory.CreateDirectory(path); // NOTE: MAKE SURE THERE ARE CREATE/WRITE PERMISSIONS
				path = System.IO.Path.Combine(path, email);
				if (System.IO.File.Exists(path))
					return null;
				await System.IO.File.WriteAllTextAsync(path, CreateHash(password));
				return new User(email);
			}
			catch
			{
				return null;
			}
		}
	}
}

Here is the Program.cs file of the server project. The secret key can be stored in appsettings.json. Notice the code comments.

// Program.cs
using BlazorExample;
using BlazorExample.Components;


using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: THIS LINE OF CODE IS NEWLY ADDED
using Microsoft.IdentityModel.Tokens; // NOTE: THIS LINE OF CODE IS NEWLY ADDED


var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
	.AddInteractiveServerComponents()
	.AddInteractiveWebAssemblyComponents();



builder.Services.AddControllers(); // NOTE: line is newly added

builder.Services.AddTransient<IUserDatabase, UserDatabase>();  // NOTE: LOCAL AUTHENTICATION ADDED HERE; AddTransient() IS OK TO USE BECAUSE STATE IS SAVED TO THE DRIVE

// NOTE: the following block of code is newly added
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
	options.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateAudience = true,
		ValidAudience = "domain.com", // NOTE: ENTER DOMAIN HERE
		ValidateIssuer = true,
		ValidIssuer = "domain.com", // NOTE: ENTER DOMAIN HERE
		ValidateLifetime = true,
		ValidateIssuerSigningKey = true,
		IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; A GUID IS RECOMMENDED, DO NOT REUSE THIS GUID
	};
});
// NOTE: end new block of code



var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
	app.UseWebAssemblyDebugging();
}
else
{
	app.UseExceptionHandler("/Error", createScopeForErrors: true);
	// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
	app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.UseAuthentication(); // NOTE: line is newly added

app.MapRazorComponents<App>()
	.AddInteractiveServerRenderMode()
	.AddInteractiveWebAssemblyRenderMode()
	.AddAdditionalAssemblies(typeof(BlazorExample.Client._Imports).Assembly); // NOTE: This line has been modified

app.MapControllers(); // NOTE: line is newly added

app.UseAuthorization(); // NOTE: line is newly added

app.Run();

Create a new folder named Controllers.

Here is the authentication controller file located in the Controllers folder of the server project. It is named AuthController.cs and it will fill in the User.Identity.Name field for methods that are marked [Authorize].

// AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using BlazorExample.Client;

namespace BlazorExample.Controllers
{
	[ApiController]
	public class AuthController : ControllerBase
	{
		private string CreateJWT(User user)
		{
			var secretkey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")); // NOTE: SAME KEY AS USED IN Program.cs FILE; DO NOT REUSE THIS GUID
			var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256);

			var claims = new[] // NOTE: could also use List<Claim> here
			{
				new Claim(ClaimTypes.Name, user.Email), // NOTE: this will be the "User.Identity.Name" value
				new Claim(JwtRegisteredClaimNames.Sub, user.Email),
				new Claim(JwtRegisteredClaimNames.Email, user.Email),
				new Claim(JwtRegisteredClaimNames.Jti, user.Email) // NOTE: this could a unique ID assigned to the user by a database
			};

			var token = new JwtSecurityToken(issuer: "domain.com", audience: "domain.com", claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: credentials); // NOTE: ENTER DOMAIN HERE
			var jsth = new JwtSecurityTokenHandler();
			return jsth.WriteToken(token);
		}

		private IUserDatabase userdb { get; }

		public AuthController(IUserDatabase userdb)
		{
			this.userdb = userdb;
		}

		[HttpPost]
		[Route("api/auth/register")]
		public async Task<LoginResult> Post([FromBody] RegModel reg)
		{
			if (reg.password != reg.confirmpwd)
				return new LoginResult { message = "Password and confirm password do not match.", success = false };
			User newuser = await userdb.AddUser(reg.email, reg.password);
			if (newuser != null)
				return new LoginResult { message = "Registration successful.", jwtBearer = CreateJWT(newuser), email = reg.email, success = true };
			return new LoginResult { message = "User already exists.", success = false };
		}

		[HttpPost]
		[Route("api/auth/login")]
		public async Task<LoginResult> Post([FromBody] LoginModel log)
		{
			User user = await userdb.AuthenticateUser(log.email, log.password);
			if (user != null)
				return new LoginResult { message = "Login successful.", jwtBearer = CreateJWT(user), email = log.email, success = true };
			return new LoginResult { message = "User/password not found.", success = false };
		}

	}
}

Create a new file named WeatherForecastController.cs located in the Controllers folder of the server project.

// WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.AspNetCore.Authorization;
using BlazorExample.Client;

namespace BlazorExample.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

		[HttpGet]
		[Authorize]
		public IEnumerable<WeatherForecast> Get()
		{
			var rng = new Random();
			return Enumerable.Range(1, 5).Select(index => new WeatherForecast
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity?.Name ?? string.Empty
			});
		}

		[HttpGet("{date}")]
		[Authorize]
		public WeatherForecast Get(DateTime date)
		{
			var rng = new Random();
			return new WeatherForecast
			{
				Date = date,
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity?.Name ?? string.Empty
			};
		}

	}
}

Modify the NavMenu.razor component to have a new link as done in the code below.

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorExample</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        @* NOTE: THIS BLOCK OF CODE IS NEW *@
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        @* NOTE: END BLOCK OF NEW CODE *@

    </nav>
</div>

Modify the Client Project's Code (Again)

Razor Components

Here is the one razor component created for the client project. It is called UserComponent and the file is named UserComponent.razor. It should be located in the folder named Pages.

@inject IJSRuntime jsr

<p>
	@if (string.IsNullOrEmpty(username))
	{
		<span><a href="/register">Register</a> <a href="/login">Login</a></span>
	}
	else
	{
		<span>Hello, @username <a href="/logout">(Logout)</a></span>
	}
</p>

@code {

	private string username = string.Empty;
	private System.Threading.Timer? timer;

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		timer = new System.Threading.Timer(async (object? stateInfo) =>
		{
			var userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false) ?? string.Empty;
			var temp = userdata.Split(';', 2)[0];
			if (temp != username)
			{
				username = temp;
				StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
			}
		}, new System.Threading.AutoResetEvent(false), 333, 333);

	}
}

Razor Pages

Here is the Register.razor page for the client project.

@page "/register"
@rendermode InteractiveAuto
@inject NavigationManager nav

<h3>Register</h3>

<p>@message</p>
<p><a href="/login">@login</a></p>

<EditForm Model="reg" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="reg.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.confirmpwd" placeholder="Confirm Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">register</button>
	</div>
</EditForm>

@code {

	RegModel reg = new RegModel();
	string message = string.Empty, login = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		isDisabled = true;
		using(var Http = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
		{
			using (var msg = await System.Net.Http.Json.HttpClientJsonExtensions.PostAsJsonAsync<RegModel>(Http, "/api/auth/register", reg, System.Threading.CancellationToken.None))
			{
				if (msg.IsSuccessStatusCode)
				{
					LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
					message = result.message;
					if (result.success)
					{
						message += " Please LOGIN to continue.";
						login = "Click here to LOGIN.";
					}
					else
						isDisabled = false;
				}
			}
		}
	}
}

Here is the Login.razor page for the client project.

@page "/login"
@rendermode InteractiveAuto
@inject IJSRuntime jsr
@inject NavigationManager nav

<h3>Login</h3>

<p>@message</p>

<EditForm Model="user" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="user.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="user.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">login</button>
	</div>
</EditForm>

@code {

	LoginModel user = new LoginModel();
	string message = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		using(HttpClient Http = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
		{

			isDisabled = true;
			using (var msg = await Http.PostAsJsonAsync<LoginModel>("/api/auth/login", user, System.Threading.CancellationToken.None))
			{
				if (msg.IsSuccessStatusCode)
				{
					LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
					message = result.message;
					isDisabled = false;
					if (result.success)
						await jsr.InvokeVoidAsync("localStorage.setItem", "user", $"{result.email};{result.jwtBearer}").ConfigureAwait(false);
				}
			}
		}
	}
}

Here is the Logout.razor page for the client project.

@page "/logout"
@rendermode @(new InteractiveAutoRenderMode(prerender: false))
@inject IJSRuntime jsr
@inject NavigationManager nav

@code {

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
		nav.NavigateTo("/");
	}
}

Here is the newly created FetchData.razor page for the client project.

@page "/fetchdata"
@rendermode @(new InteractiveAutoRenderMode(prerender: false))
@inject IJSRuntime jsr
@inject NavigationManager nav

<PageTitle>FetchData</PageTitle>

<UserComponent></UserComponent>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>
@if (string.IsNullOrEmpty(userdata))
{
	<p><a href="/login">LOGIN TO ACCESS THIS DATA</a></p>
}
else
{
	if (forecasts == null)
	{
		<p><em>Loading...</em></p>
	}
	else
	{
		<p><a href="javascript:;" @onclick="GetTodaysForecast">TODAY'S FORECAST</a></p>
		<table class="table">
			<thead>
				<tr>
					<th>Date</th>
					<th>Temp. (C)</th>
					<th>Temp. (F)</th>
					<th>Summary</th>
					<th>User</th>
				</tr>
			</thead>
			<tbody>
				@foreach (var forecast in forecasts)
				{
					<tr>
						<td>@forecast.Date.ToShortDateString()</td>
						<td>@forecast.TemperatureC</td>
						<td>@forecast.TemperatureF</td>
						<td>@forecast.Summary</td>
						<td>@forecast.UserName</td>
					</tr>
				}
			</tbody>
		</table>
	}
}

@code {

	private List<WeatherForecast> forecasts;
	string userdata;

	private async Task<string> GetJWT()
	{
		userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false);
		if (!string.IsNullOrWhiteSpace(userdata))
		{
			var dataArray = userdata.Split(';', 2);
			if (dataArray.Length == 2)
				return dataArray[1];
		}
		return null;
	}

	private async Task GetTodaysForecast()
	{
		try
		{
			using(HttpClient Http = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
			{

				var requestMsg = new HttpRequestMessage(HttpMethod.Get, $"/api/weatherforecast/{DateTime.Now.ToString("yyyy-MM-dd")}");
				requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
				var response = await Http.SendAsync(requestMsg);
				if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
				{
					await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
					userdata = null;
				}
				else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
					forecasts = null;
				else if (response.IsSuccessStatusCode)
				{
					var forecast = await response.Content.ReadFromJsonAsync<WeatherForecast>();
					forecasts.Clear();
					forecasts.Add(forecast);
				}
			}
		}
		catch (Exception ex)
		{

		}
	}

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		try
		{
			using(HttpClient Http = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
			{
				var requestMsg = new HttpRequestMessage(HttpMethod.Get, "/api/weatherforecast");
				requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
				var response = await Http.SendAsync(requestMsg);
				if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
				{
					await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
					userdata = null;
				}
				else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
					forecasts = null;
				else if (response.IsSuccessStatusCode)
					forecasts = await response.Content.ReadFromJsonAsync<List<WeatherForecast>>();
			}
			catch (Exception ex)
			{
				string err = ex.Message;
			}
		}
	}
}

The project's Solution Explorer should look like this if the project was created from scratch:

Blazor Web App Solution Explorer

.NET 6 Example

As in the below video:
  1. Create a new Blazor WebAssembly application called "BlazorExample" in this example.
  2. The next step is to add the right NuGet packages to the Server, Client and Shared projects.
    1. Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the server project. Pay attention to the version, 6.0.x in this case.
    2. Add the System.ComponentModel.Annotations NuGet package to the client project.
    3. Add the System.ComponentModel.Annotations NuGet package to the C# shared project.

Modify the Shared Project's Code

Modify the WeatherForecast.cs file located in the root of the shared project.

// WeatherForecast.cs

namespace BlazorExample.Shared
{
	public class WeatherForecast
	{
		public DateTime Date { get; set; }

		public int TemperatureC { get; set; }

		public string Summary { get; set; }

		public string UserName { get; set; } // NOTE: THIS LINE IS NEWLY ADDED

		public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
	}
}

Here is the new AuthModel.cs file located in the root of the shared project.

// AuthModel.cs
using System.ComponentModel.DataAnnotations;

namespace BlazorExample.Shared
{
	public class LoginResult
	{
		public string message { get; set; }
		public string email { get; set; }
		public string jwtBearer { get; set; }
		public bool success { get; set; }
	}
	public class LoginModel
	{
		[Required(ErrorMessage = "Email is required.")]
		[EmailAddress(ErrorMessage = "Email address is not valid.")]
		public string email { get; set; } // NOTE: email will be the username, too

		[Required(ErrorMessage = "Password is required.")]
		[DataType(DataType.Password)]
		public string password { get; set; }
	}
	public class RegModel : LoginModel
	{
		[Required(ErrorMessage = "Confirm password is required.")]
		[DataType(DataType.Password)]
		[Compare("password", ErrorMessage = "Password and confirm password do not match.")]
		public string confirmpwd { get; set; }
	}
}

Modify the Server Project's Code

Here is the UserDatabase.cs file located in the server project. It is a crude example of a database for storing users. It is file-system based. At this point one should already be familiar with using the database of their choice. The decision not to use a database in this example is done to make the example focus purely on JWT. See this article for help using Microsoft's SQL Server database.

// UserDatabase.cs
using System.Security.Cryptography;
using System.Text;

namespace BlazorExample.Server
{
	public class User
	{
		public string Email { get; }
		public User(string email)
		{
			Email = email;
		}
	}
	public interface IUserDatabase
	{
		Task<User> AuthenticateUser(string email, string password);
		Task<User> AddUser(string email, string password);
	}
	public class UserDatabase : IUserDatabase
	{
		private readonly IWebHostEnvironment env;
		public UserDatabase(IWebHostEnvironment env) => this.env = env;
		private static string CreateHash(string password)
		{
			var salt = "997eff51db1544c7a3c2ddeb2053f052";
			var md5 = new HMACSHA256(Encoding.UTF8.GetBytes(salt + password));
			byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
			return System.Convert.ToBase64String(data);
		}
		public async Task<User> AuthenticateUser(string email, string password)
		{
			if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
				return null;
			var path = System.IO.Path.Combine(env.ContentRootPath, "Users");
			if (!System.IO.Directory.Exists(path))
				return null;
			path = System.IO.Path.Combine(path, email);
			if (!System.IO.File.Exists(path))
				return null;
			if (await System.IO.File.ReadAllTextAsync(path) != CreateHash(password))
				return null;
			return new User(email);
		}
		public async Task<User> AddUser(string email, string password)
		{
			try
			{
				if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
					return null;
				var path = System.IO.Path.Combine(env.ContentRootPath, "Users"); // NOTE: THIS WILL CREATE THE "USERS" FOLDER IN THE PROJECT'S FOLDER!!!
				if (!System.IO.Directory.Exists(path))
					System.IO.Directory.CreateDirectory(path); // NOTE: MAKE SURE THERE ARE CREATE/WRITE PERMISSIONS
				path = System.IO.Path.Combine(path, email);
				if (System.IO.File.Exists(path))
					return null;
				await System.IO.File.WriteAllTextAsync(path, CreateHash(password));
				return new User(email);
			}
			catch
			{
				return null;
			}
		}
	}
}

Here is the Program.cs file of the server project. The secret key can be stored in appsettings.json. Notice the code comments.

// Program.cs
using Microsoft.AspNetCore.ResponseCompression;



using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: THIS LINE OF CODE IS NEWLY ADDED
using Microsoft.IdentityModel.Tokens; // NOTE: THIS LINE OF CODE IS NEWLY ADDED
using BlazorExample.Server; // NOTE: THIS LINE OF CODE IS NEWLY ADDED



var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddTransient<IUserDatabase, UserDatabase>();  // NOTE: LOCAL AUTHENTICATION ADDED HERE; AddTransient() IS OK TO USE BECAUSE STATE IS SAVED TO THE DRIVE

// NOTE: the following block of code is newly added
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
	options.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateAudience = true,
		ValidAudience = "domain.com", // NOTE: ENTER DOMAIN HERE
		ValidateIssuer = true,
		ValidIssuer = "domain.com", // NOTE: ENTER DOMAIN HERE
		ValidateLifetime = true,
		ValidateIssuerSigningKey = true,
		IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; A GUID IS RECOMMENDED. DO NOT REUSE THIS GUID.
	};
});
// NOTE: end block


builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
	app.UseWebAssemblyDebugging();
}
else
{
	app.UseExceptionHandler("/Error");
	// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
	app.UseHsts();
}

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();
app.UseStaticFiles();


app.UseAuthentication(); // NOTE: line is newly added


app.UseRouting();


app.UseAuthorization(); // NOTE: line is newly addded, notice placement after UseRouting()


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

Here is the authentication controller file located in the Controllers folder of the server project. It is named AuthController.cs and it will fill in the User.Identity.Name field for methods that are marked [Authorize].

// AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using BlazorExample.Shared;

namespace BlazorExample.Server.Controllers
{
	[ApiController]
	public class AuthController : ControllerBase
	{
		private string CreateJWT(User user)
		{
			var secretkey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")); // NOTE: SAME KEY AS USED IN Program.cs FILE; DO NOT REUSE THIS GUID.
			var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256);

			var claims = new[] // NOTE: could also use List<Claim> here
			{
				new Claim(ClaimTypes.Name, user.Email), // NOTE: this will be the "User.Identity.Name" value
				new Claim(JwtRegisteredClaimNames.Sub, user.Email),
				new Claim(JwtRegisteredClaimNames.Email, user.Email),
				new Claim(JwtRegisteredClaimNames.Jti, user.Email) // NOTE: this could a unique ID assigned to the user by a database
			};

			var token = new JwtSecurityToken(issuer: "domain.com", audience: "domain.com", claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: credentials); // NOTE: ENTER DOMAIN HERE
			return new JwtSecurityTokenHandler().WriteToken(token);
		}

		private IUserDatabase userdb { get; }

		public AuthController(IUserDatabase userdb)
		{
			this.userdb = userdb;
		}

		[HttpPost]
		[Route("api/auth/register")]
		public async Task<LoginResult> Post([FromBody] RegModel reg)
		{
			if (reg.password != reg.confirmpwd)
				return new LoginResult { message = "Password and confirm password do not match.", success = false };
			User newuser = await userdb.AddUser(reg.email, reg.password);
			if (newuser != null)
				return new LoginResult { message = "Registration successful.", jwtBearer = CreateJWT(newuser), email = reg.email, success = true };
			return new LoginResult { message = "User already exists.", success = false };
		}

		[HttpPost]
		[Route("api/auth/login")]
		public async Task<LoginResult> Post([FromBody] LoginModel log)
		{
			User user = await userdb.AuthenticateUser(log.email, log.password);
			if (user != null)
				return new LoginResult { message = "Login successful.", jwtBearer = CreateJWT(user), email = log.email, success = true };
			return new LoginResult { message = "User/password not found.", success = false };
		}
	}
}

This is WeatherForecastController.cs located in the Controllers folder of the server project.

// WeatherForecastController.cs
using BlazorExample.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.AspNetCore.Authorization;

namespace BlazorExample.Server.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

		[HttpGet]
		[Authorize] // NOTE: THIS LINE OF CODE IS NEWLY ADDED
		public IEnumerable<WeatherForecast> Get()
		{
			var rng = new Random();
			return Enumerable.Range(1, 5).Select(index => new WeatherForecast
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity?.Name ?? string.Empty // NOTE: THIS LINE OF CODE IS NEWLY ADDED
			});
		}

		// NOTE: THIS ENTIRE BLOCK OF CODE IS NEWLY ADDED
		[HttpGet("{date}")]
		[Authorize]
		public WeatherForecast Get(DateTime date)
		{
			var rng = new Random();
			return new WeatherForecast
			{
				Date = date,
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity?.Name ?? string.Empty
			};
		}
		// NOTE: END BLOCK
	}
}

Modify the Client Project's Code

Razor Components

Here is the one razor component created for the client project. It is called UserComponent and the file is named UserComponent.razor

@inject IJSRuntime jsr

<p>
	@if (string.IsNullOrEmpty(username))
	{
		<span><a href="/register">Register</a> <a href="/login">Login</a></span>
	}
	else
	{
		<span>Hello, @username <a href="/logout">(Logout)</a></span>
	}
</p>

@code {

	private string username = string.Empty;
	private System.Threading.Timer? timer;

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		timer = new System.Threading.Timer(async (object? stateInfo) =>
		{
			var userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false) ?? string.Empty;
			var temp = userdata.Split(';', 2)[0];
			if (temp != username)
			{
				username = temp;
				StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
			}
		}, new System.Threading.AutoResetEvent(false), 333, 333);

	}
}

Razor Pages

Here is the Register.razor page for the client project.

@page "/register"
@using BlazorExample.Shared
@inject HttpClient Http

<h3>Register</h3>

<p>@message</p>
<p><a href="/login">@login</a></p>

<EditForm Model="reg" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="reg.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.confirmpwd" placeholder="Confirm Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">register</button>
	</div>
</EditForm>

@code {
	RegModel reg = new RegModel();
	string message = string.Empty, login = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		isDisabled = true;
		using (var msg = await Http.PostAsJsonAsync<RegModel>("/api/auth/register", reg, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
				message = result.message;
				if (result.success)
				{
					message += " Please LOGIN to continue.";
					login = "Click here to LOGIN.";
				}
				else
					isDisabled = false;
			}
		}
	}
}

Here is the Login.razor page for the client project.

@page "/login"
@using BlazorExample.Shared
@inject HttpClient Http
@inject IJSRuntime jsr

<h3>Login</h3>

<p>@message</p>

<EditForm Model="user" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="user.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="user.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">login</button>
	</div>
</EditForm>

@code {
	LoginModel user = new LoginModel();
	string message = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		isDisabled = true;
		using (var msg = await Http.PostAsJsonAsync<LoginModel>("/api/auth/login", user, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
				message = result.message;
				isDisabled = false;
				if (result.success)
					await jsr.InvokeVoidAsync("localStorage.setItem", "user", $"{result.email};{result.jwtBearer}").ConfigureAwait(false);
			}
		}
	}
}

Here is the Logout.razor page for the client project.

@page "/logout"
@inject IJSRuntime jsr
@inject NavigationManager nav

@code {

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
		nav.NavigateTo("/");
	}
}

Here is the modified FetchData.razor page for the client project.

@page "/fetchdata"
@using BlazorExample.Shared
@inject HttpClient Http
@inject IJSRuntime jsr

<UserComponent></UserComponent>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>
@if (string.IsNullOrEmpty(userdata))
{
	<p><a href="/login">LOGIN TO ACCESS THIS DATA</a></p>
}
else
{
	if (forecasts == null)
	{
		<p><em>Loading...</em></p>
	}
	else
	{
		<p><a href="javascript:;" @onclick="GetTodaysForecast">TODAY'S FORECAST</a></p>
		<table class="table">
			<thead>
				<tr>
					<th>Date</th>
					<th>Temp. (C)</th>
					<th>Temp. (F)</th>
					<th>Summary</th>
					<th>User</th>
				</tr>
			</thead>
			<tbody>
				@foreach (var forecast in forecasts)
				{
					<tr>
						<td>@forecast.Date.ToShortDateString()</td>
						<td>@forecast.TemperatureC</td>
						<td>@forecast.TemperatureF</td>
						<td>@forecast.Summary</td>
						<td>@forecast.UserName</td>
					</tr>
				}
			</tbody>
		</table>
	}
}

@code {

	private List<WeatherForecast> forecasts;
	string userdata;

	private async Task<string> GetJWT()
	{
		userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false);
		if (!string.IsNullOrWhiteSpace(userdata))
		{
			var dataArray = userdata.Split(';', 2);
			if (dataArray.Length == 2)
				return dataArray[1];
		}
		return null;
	}

	private async Task GetTodaysForecast()
	{
		try
		{
			var requestMsg = new HttpRequestMessage(HttpMethod.Get, $"/api/weatherforecast/{DateTime.Now.ToString("yyyy-MM-dd")}");
			requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
			var response = await Http.SendAsync(requestMsg);
			if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
			{
				await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
				userdata = null;
			}
			else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
				forecasts = null;
			else if (response.IsSuccessStatusCode)
			{
				var forecast = await response.Content.ReadFromJsonAsync<WeatherForecast>();
				forecasts.Clear();
				forecasts.Add(forecast);
			}
		}
		catch (Exception ex)
		{

		}
	}

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		try
		{
			var requestMsg = new HttpRequestMessage(HttpMethod.Get, "/api/weatherforecast");
			requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
			var response = await Http.SendAsync(requestMsg);
			if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
			{
				await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
				userdata = null;
			}
			else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
				forecasts = null;
			else if (response.IsSuccessStatusCode)
				forecasts = await response.Content.ReadFromJsonAsync<List<WeatherForecast>>();
		}
		catch (Exception ex)
		{

		}
	}
}

.NET Core 3.1 Example

Create a new Blazor WebAssembly application called "BlazorExample" in this example.

The next step is to add the right NuGet packages to the Server, Client and Shared projects.

Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the server project. Pay attention to the version, 3.1.x in this case.

Add the System.ComponentModel.Annotations NuGet package to the client (Blazor WebAssembly) project.

Add the System.ComponentModel.Annotations NuGet package to the C# shared project.

Modify the Shared Project's Code

Modify the WeatherForecast.cs file located in the root of the shared project.

// WeatherForecast.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace BlazorExample.Shared
{
	public class WeatherForecast
	{
		public DateTime Date { get; set; }

		public int TemperatureC { get; set; }

		public string Summary { get; set; }

		public string UserName { get; set; } // NOTE: THIS LINE IS NEWLY ADDED

		public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
	}
}

Here is the new AuthModel.cs file located in the root of the shared project.

// AuthModel.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Text;

namespace BlazorExample.Shared
{
	public class LoginResult
	{
		public string message { get; set; }
		public string email { get; set; }
		public string jwtBearer { get; set; }
		public bool success { get; set; }
	}
	public class LoginModel
	{
		[Required(ErrorMessage = "Email is required.")]
		[EmailAddress(ErrorMessage = "Email address is not valid.")]
		public string email { get; set; } // NOTE: email will be the username, too

		[Required(ErrorMessage = "Password is required.")]
		[DataType(DataType.Password)]
		public string password { get; set; }
	}
	public class RegModel : LoginModel
	{
		[Required(ErrorMessage = "Confirm password is required.")]
		[DataType(DataType.Password)]
		[Compare("password", ErrorMessage = "Password and confirm password do not match.")]
		public string confirmpwd { get; set; }
	}
}

Modify the Server Project's Code

Here is the UserDatabase.cs file located in the server project. It is a crude example of a database for storing users. It is file-system based. At this point one should already be familiar with using the database of their choice. The decision not to use a database in this example is done to make the example focus purely on JWT. See this article for help using Microsoft's SQL Server database.

// UserDatabase.cs
using Microsoft.AspNetCore.Hosting;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace BlazorExample.Server
{
	public class User
	{
		public string Email { get; }
		public User(string email)
		{
			Email = email;
		}
	}
	public interface IUserDatabase
	{
		Task<User> AuthenticateUser(string email, string password);
		Task<User> AddUser(string email, string password);
	}
	public class UserDatabase : IUserDatabase
	{
		private readonly IWebHostEnvironment env;
		public UserDatabase(IWebHostEnvironment env) => this.env = env;
		private static string CreateHash(string password)
		{
			var salt = "997eff51db1544c7a3c2ddeb2053f052";
			var md5 = new HMACSHA256(Encoding.UTF8.GetBytes(salt + password));
			byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
			return System.Convert.ToBase64String(data);
		}
		public async Task<User> AuthenticateUser(string email, string password)
		{
			if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
				return null;
			var path = System.IO.Path.Combine(env.ContentRootPath, "Users");
			if (!System.IO.Directory.Exists(path))
				return null;
			path = System.IO.Path.Combine(path, email);
			if (!System.IO.File.Exists(path))
				return null;
			if (await System.IO.File.ReadAllTextAsync(path) != CreateHash(password))
				return null;
			return new User(email);
		}
		public async Task<User> AddUser(string email, string password)
		{
			try
			{
				if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
					return null;
				var path = System.IO.Path.Combine(env.ContentRootPath, "Users"); // NOTE: THIS WILL CREATE THE "USERS" FOLDER IN THE PROJECT'S FOLDER!!!
				if (!System.IO.Directory.Exists(path))
					System.IO.Directory.CreateDirectory(path); // NOTE: MAKE SURE THERE ARE CREATE/WRITE PERMISSIONS
				path = System.IO.Path.Combine(path, email);
				if (System.IO.File.Exists(path))
					return null;
				await System.IO.File.WriteAllTextAsync(path, CreateHash(password));
				return new User(email);
			}
			catch
			{
				return null;
			}
		}
	}
}

Here is the Startup.cs of the server project.

// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: THIS LINE OF CODE IS NEWLY ADDED
using Microsoft.IdentityModel.Tokens; // NOTE: THIS LINE OF CODE IS NEWLY ADDED

namespace BlazorExample.Server
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		// This method gets called by the runtime. Use this method to add services to the container.
		// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
		public void ConfigureServices(IServiceCollection services)
		{

			services.AddControllersWithViews();
			services.AddRazorPages();

			// NOTE: LOCAL AUTHENTICATION CODE IS BELOW THIS LINE BUT INSIDE THIS BLOCK
			services.AddTransient<IUserDatabase, UserDatabase>();  // NOTE: LOCAL AUTHENTICATION ADDED HERE; AddTransient() IS OK TO USE BECAUSE STATE IS SAVED TO THE DRIVE

			// NOTE: the following lines are newly added
			services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
			{
				options.TokenValidationParameters = new TokenValidationParameters
				{
					ValidateAudience = true,
					ValidAudience = "domain.com", // NOTE: ENTER DOMAIN HERE
					ValidateIssuer = true,
					ValidIssuer = "domain.com", // NOTE: ENTER DOMAIN HERE
					ValidateLifetime = true,
					ValidateIssuerSigningKey = true,
					IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; DO NOT REUSE THIS GUID.
				};
			});
		}

		// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
		public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
		{
			if (env.IsDevelopment())
			{
				app.UseDeveloperExceptionPage();
				app.UseWebAssemblyDebugging();
			}
			else
			{
				app.UseExceptionHandler("/Error");
			}

			app.UseBlazorFrameworkFiles();

			app.UseStaticFiles();

			app.UseAuthentication(); // NOTE: line is newly added

			app.UseRouting();

			app.UseAuthorization(); // NOTE: line is newly addded, notice placement between UseRouting() and UseEndpoints()

			app.UseEndpoints(endpoints =>
			{
				endpoints.MapRazorPages();
				endpoints.MapControllers();
				endpoints.MapFallbackToFile("index.html");
			});
		}
	}
}

Here is the authentication controller file located in the Controllers folder of the server project. It is named AuthController.cs and it will fill in the User.Identity.Name field for methods that are marked [Authorize].

// AuthController.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using BlazorExample.Server;
using BlazorExample.Shared;

namespace BlazorExample.Server.Controllers
{
	[ApiController]
	public class AuthController : ControllerBase
	{
		private string CreateJWT(User user)
		{
			var secretkey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("524C1F22-6115-4E16-9B6A-3FBF185308F2")); // NOTE: SAME KEY AS USED IN Startup.cs FILE; DO NOT REUSE THIS GUID
			var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256);

			var claims = new[] // NOTE: could also use List<Claim> here
			{
				new Claim(ClaimTypes.Name, user.Email), // NOTE: this will be the "User.Identity.Name" value
				new Claim(JwtRegisteredClaimNames.Sub, user.Email),
				new Claim(JwtRegisteredClaimNames.Email, user.Email),
				new Claim(JwtRegisteredClaimNames.Jti, user.Email) // NOTE: this could a unique ID assigned to the user by a database
			};

			var token = new JwtSecurityToken(issuer: "domain.com", audience: "domain.com", claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: credentials); // NOTE: ENTER DOMAIN HERE
			return new JwtSecurityTokenHandler().WriteToken(token);
		}

		private IUserDatabase userdb { get; }

		public AuthController(IUserDatabase userdb)
		{
			this.userdb = userdb;
		}

		[HttpPost]
		[Route("api/auth/register")]
		public async Task<LoginResult> Post([FromBody] RegModel reg)
		{
			if (reg.password != reg.confirmpwd)
				return new LoginResult { message = "Password and confirm password do not match.", success = false };
			User newuser = await userdb.AddUser(reg.email, reg.password);
			if (newuser != null)
				return new LoginResult { message = "Registration successful.", jwtBearer = CreateJWT(newuser), email = reg.email, success = true };
			return new LoginResult { message = "User already exists.", success = false };
		}

		[HttpPost]
		[Route("api/auth/login")]
		public async Task<LoginResult> Post([FromBody] LoginModel log)
		{
			User user = await userdb.AuthenticateUser(log.email, log.password);
			if (user != null)
				return new LoginResult { message = "Login successful.", jwtBearer = CreateJWT(user), email = log.email, success = true };
			return new LoginResult { message = "User/password not found.", success = false };
		}
	}
}

This is WeatherForecastController.cs located in the Controllers folder of the server project.

// WeatherForecastController.cs
using BlazorExample.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.AspNetCore.Authorization;

namespace BlazorExample.Server.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

		[HttpGet]
		[Authorize] // NOTE: THIS LINE OF CODE IS NEWLY ADDED
		public IEnumerable<WeatherForecast> Get()
		{
			var rng = new Random();
			return Enumerable.Range(1, 5).Select(index => new WeatherForecast
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity.Name // NOTE: THIS LINE OF CODE IS NEWLY ADDED
			});
		}

		// NOTE: THIS ENTIRE METHOD IS NEWLY ADDED
		[HttpGet("{date}")]
		[Authorize]
		public WeatherForecast Get(DateTime date)
		{
			var rng = new Random();
			return new WeatherForecast
			{
				Date = date,
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)],
				UserName = User.Identity.Name
			};
		}
	}
}

Modify the Client Project's Code

Razor Components

Here is the one razor component created for the client project. It is called UserComponent and the file is named UserComponent.razor

@inject IJSRuntime jsr

<p>
	@if (string.IsNullOrEmpty(username))
	{
		<span><a href="/register">Register</a> <a href="/login">Login</a></span>
	}
	else
	{
		<span>Hello, @username <a href="/logout">(Logout)</a></span>
	}
</p>

@code {

	private string username = string.Empty;
	private System.Threading.Timer? timer;

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		timer = new System.Threading.Timer(async (object? stateInfo) =>
		{
			var userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false) ?? string.Empty;
			var temp = userdata.Split(';', 2)[0];
			if (temp != username)
			{
				username = temp;
				StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
			}
		}, new System.Threading.AutoResetEvent(false), 333, 333);

	}
}

Razor Pages

Here is the Register.razor page for the client project.

@page "/register"
@using BlazorExample.Shared
@inject HttpClient Http

<h3>Register</h3>

<p>@message</p>
<p><a href="/login">@login</a></p>

<EditForm Model="reg" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="reg.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="reg.confirmpwd" placeholder="Confirm Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">register</button>
	</div>
</EditForm>

@code {
	RegModel reg = new RegModel();
	string message = string.Empty, login = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		isDisabled = true;
		using (var msg = await Http.PostAsJsonAsync<RegModel>("/api/auth/register", reg, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
				message = result.message;
				if (result.success)
				{
					message += " Please LOGIN to continue.";
					login = "Click here to LOGIN.";
				}
				else
					isDisabled = false;
			}
		}
	}
}

Here is the Login.razor page for the client project.

@page "/login"
@using BlazorExample.Shared
@inject HttpClient Http
@inject IJSRuntime jsr

<h3>Login</h3>

<p>@message</p>

<EditForm Model="user" OnValidSubmit="OnValid" style="max-width:500px;">
	<DataAnnotationsValidator />
	<ValidationSummary />
	<div class="mb-2">
		<InputText class="form-control" @bind-Value="user.email" placeholder="Enter Email"></InputText>
	</div>
	<div class="mb-2">
		<InputText type="password" class="form-control" @bind-Value="user.password" placeholder="Enter Password"></InputText>
	</div>
	<div class="mb-2 text-right">
		<button class="btn btn-secondary" disabled="@isDisabled">login</button>
	</div>
</EditForm>

@code {
	LoginModel user = new LoginModel();
	string message = string.Empty;
	bool isDisabled = false;

	private async Task OnValid()
	{
		isDisabled = true;
		using (var msg = await Http.PostAsJsonAsync<LoginModel>("/api/auth/login", user, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();
				message = result.message;
				isDisabled = false;
				if (result.success)
					await jsr.InvokeVoidAsync("localStorage.setItem", "user", $"{result.email};{result.jwtBearer}").ConfigureAwait(false);
			}
		}
	}
}

Here is the Logout.razor page for the client project.

@page "/logout"
@inject IJSRuntime jsr
@inject NavigationManager nav

@code {

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
		nav.NavigateTo("/");
	}
}

Here is the modified FetchData.razor page for the client project.

@page "/fetchdata"
@using BlazorExample.Shared
@inject HttpClient Http
@inject IJSRuntime jsr

<UserComponent></UserComponent>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>
@if (string.IsNullOrEmpty(userdata))
{
	<p><a href="/login">LOGIN TO ACCESS THIS DATA</a></p>
}
else
{
	if (forecasts == null)
	{
		<p><em>Loading...</em></p>
	}
	else
	{
		<p><a href="javascript:;" @onclick="GetTodaysForecast">TODAY'S FORECAST</a></p>
		<table class="table">
			<thead>
				<tr>
					<th>Date</th>
					<th>Temp. (C)</th>
					<th>Temp. (F)</th>
					<th>Summary</th>
					<th>User</th>
				</tr>
			</thead>
			<tbody>
				@foreach (var forecast in forecasts)
				{
					<tr>
						<td>@forecast.Date.ToShortDateString()</td>
						<td>@forecast.TemperatureC</td>
						<td>@forecast.TemperatureF</td>
						<td>@forecast.Summary</td>
						<td>@forecast.UserName</td>
					</tr>
				}
			</tbody>
		</table>
	}
}

@code {

	private List<WeatherForecast> forecasts;
	string userdata;

	private async Task<string> GetJWT()
	{
		userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false);
		if (!string.IsNullOrWhiteSpace(userdata))
		{
			var dataArray = userdata.Split(';', 2);
			if (dataArray.Length == 2)
				return dataArray[1];
		}
		return null;
	}

	private async Task GetTodaysForecast()
	{
		try
		{
			var requestMsg = new HttpRequestMessage(HttpMethod.Get, $"/api/weatherforecast/{DateTime.Now.ToString("yyyy-MM-dd")}");
			requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
			var response = await Http.SendAsync(requestMsg);
			if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
			{
				await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
				userdata = null;
			}
			else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
				forecasts = null;
			else if (response.IsSuccessStatusCode)
			{
				var forecast = await response.Content.ReadFromJsonAsync<WeatherForecast>();
				forecasts.Clear();
				forecasts.Add(forecast);
			}
		}
		catch (Exception ex)
		{

		}
	}

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		try
		{
			var requestMsg = new HttpRequestMessage(HttpMethod.Get, "/api/weatherforecast");
			requestMsg.Headers.Add("Authorization", "Bearer " + await GetJWT());
			var response = await Http.SendAsync(requestMsg);
			if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
			{
				await jsr.InvokeVoidAsync("localStorage.removeItem", "user").ConfigureAwait(false);
				userdata = null;
			}
			else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
				forecasts = null;
			else if (response.IsSuccessStatusCode)
				forecasts = await response.Content.ReadFromJsonAsync<List<WeatherForecast>>();
		}
		catch (Exception ex)
		{

		}
	}
}

Coding Video

NOTE: the above code is slightly modified from what is presented in the video.

https://youtu.be/xukWLqTGqgA


This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE