PROWAREtech
Blazor: Json Web Token (JWT) Authentication Example - Simple
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.
The next step is to add the correct NuGet packages to the Server and Client projects.
- Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the server project. Pay attention to the version, 8.0.x in this case.
- Add the System.ComponentModel.Annotations NuGet package to the client project.
These are the Blazor Web App Server-side installed NuGet packages:
These are the Blazor Web App Client-side installed 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:
.NET 6 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, 6.0.x in this case.
- Add the System.ComponentModel.Annotations NuGet package to the client 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
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.