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 6/Visual Studio 2022 or .NET Core 3.1/Visual Studio 2019.
Develop this application on Linux!
A more complete example of the same topic this page covers can be found here, at this link, but it is more complex.
.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
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
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 HMACMD5(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. 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",
ValidateIssuer = true,
ValidIssuer = "domain.com",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("THIS IS THE SECRET KEY")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; A GUID IS RECOMMENDED
};
});
// 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("THIS IS THE SECRET KEY")); // NOTE: SAME KEY AS USED IN Program.cs FILE
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);
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 (Blazor WASM)
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 {
string username = string.Empty;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(userdata))
{
username = userdata.Split(';', 2)[0];
}
}
}
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
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
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 HMACMD5(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",
ValidateIssuer = true,
ValidIssuer = "domain.com",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("THIS IS THE SECRET KEY")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED
};
});
}
// 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("THIS IS THE SECRET KEY")); // NOTE: SAME KEY AS USED IN Startup.cs FILE
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);
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 (Blazor WASM)
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 {
string username = string.Empty;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var userdata = await jsr.InvokeAsync<string>("localStorage.getItem", "user").ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(userdata))
{
username = userdata.Split(';', 2)[0];
}
}
}
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.