ASP.NET Core 5 and Blazor Web Assembly Apps: Token Based Authentication in ASP.NET Core 5 APIs and Authenticating it using Blazor WebAssembly Apps
Recently, while conducting training in .NET 5 and Blazor, my students have asked me to show the implementation of JSON Web Token based Authentication for ASP.NET Core 5 API and accessing this secure API using Blazor WebAssembly client apps. After the explanation and demo, I thought to write a tutorial on it. I have already posted an article on the ASP.NET Core 3.1 JSON Web Token with concept of JSON Web Token and implementation. You can read the article form this post. For all the new readers, I have provided all steps of implementation Token based authentication in ASP.NET Core 5 API and accessing the API in Blazor WebAssembly application. The figure 1 explains all steps of the implementation
Figure 1: The Blazor WebAssembly Application accessing Token Based Secure API developed using ASP.NET Core 5
As shown in the figure 1, we will be having Identity database which will store Identity Users, Roles, etc. The application database will store application data e.g. Products Info, etc. The API will have Controllers for managing Identity for the application users e.g. Registering New Users and Authenticating user. The Business App Controller will perform Business operations against Application database. The Blazor WebAssembly client application contains components for User Management and Accessing Business Application Data. The Step-By-Step execution is explained in following points
- The Register New User Component is used to make HTTP Request to AuthAPI Controller to register new user.
- The AuthAPI controller connects to the Identity database and stores the Users information in AspNetUsers Table if the user is not already exists.
- The Login Components sends the users credentials to the AuthAPI controller to Authenticate user.
- The AuthAPI controller connects to the Identity database and check if the user exists and the password matches.
- Based on the Credentials check, following possible operations will takes place
- 5a. If the authentication fails or user does not exist, then the error response will be send back to the Login Component
- 5b. If the Authentication is successful, then the token will be generated.
- The JSON Web Token will be send back to the client
- The client application has to store the token in its own process (or in sessionStorage, localstorage or in state). The Data Read/Write component will use this token to authenticate requests.
- The Data Read/Write component will make call to Business API by sending the token in HTTP Header.
- The Business API controller will accept the token and verify the token.
- Based on the Token verifications, following operations will takes place
- 10a. If the Token verification failed, then the Unauthorized response will be send to client
- 10b. If the Token is verified, then the data read/write operations will take place and response will be send back to the client.
using System.ComponentModel.DataAnnotations; namespace SharedModels.Models { public class LoginUser { [Required(ErrorMessage = "User Name is Must")] public string UserName { get; set; } [Required(ErrorMessage = "Password is Must")] public string Password { get; set; } } public class RegisterUser { [Required(ErrorMessage = "Email is Must")] [EmailAddress] public string Email { get; set; } [Required(ErrorMessage = "Password is Must")] [RegularExpression("^((?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])| (?=.*?[A-Z])(?=.*?[a-z])(?=.*?[^a-zA-Z0-9])|(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])| (?=.*?[a-z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])).{8,}$", ErrorMessage = "Passwords must be minimum 8 characters and must be string password with uppercase character, number and sepcial character")] public string Password { get; set; } [Compare("Password")] public string ConfirmPassword { get; set; } } public class ResponseData { public string Message { get; set; } } public class Product { public int ProductId { get; set; } public string ProductName { get; set; } public int Price { get; set; } } }
- RegisterUser class will be used to register new user.
- LoginUser class will be used to authenticate user
- ResponseData class represents the response format from API to the client application
- Product class will be used to read products information from API and send it to client
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.EntityFrameworkCore.Relational
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- This package will be used to generate ASP.NET Core Identity tables in SQL Server database.
- This will provide access to following classes
- UserManager<IdentityUser>, to create a new User
- SignInManager<IdentityUser>, to manage user login
- Microsoft.AspNetCore.Authentication.jwtBearer
- System.IdentityModel.Tokens.Jwt
public class ProductList : List<Product> { public ProductList() { Add(new Product { ProductId=1, ProductName ="P1",Price=11000}); Add(new Product { ProductId = 2, ProductName = "P2", Price = 12000 }); } }
public class JwtSecurityDbContext : IdentityDbContext { public JwtSecurityDbContext(DbContextOptions<JwtSecurityDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); } }
"ConnectionStrings": { "JwtConnectionString": "Data Source=.;Initial Catalog=JwtSecurityDb;Integrated Security=SSPI" }
services.AddDbContext<JwtSecurityDbContext>(options => { options.UseSqlServer(Configuration.GetConnectionString("JwtConnectionString")); }); services.AddIdentity<IdentityUser,IdentityRole>( options => options.SignIn.RequireConfirmedAccount = false) .AddEntityFrameworkStores<JwtSecurityDbContext>();
"JWTSettings": { "SecretKey": "MNyFyANIvTjwFW2StGH73ez1Rf1jGQD0as9+NxE2cor4wwUapS6J3QCqDWQkxwzs8FW8pFpw/0R69aVD8qsWuA==", "ExpiryInMinuts": 20 }
using System; using System.Security.Cryptography; namespace KeyGenerator { class Program { static void Main(string[] args) { using (var rNGCryptoServiceProvider = new RNGCryptoServiceProvider()) { var SigningSecretKey = new byte[64]; rNGCryptoServiceProvider.GetBytes(SigningSecretKey); Console.WriteLine($"Secret Key is {Convert.ToBase64String(SigningSecretKey)}"); } Console.ReadLine(); } } }
using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using SharedModels.Models; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Threading.Tasks; namespace SecureAPI.Services { public class AuthService { IConfiguration configuration; SignInManager<IdentityUser> signInManager; UserManager<IdentityUser> userManager; //1. public AuthService(IConfiguration configuration, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) { this.configuration = configuration; this.signInManager = signInManager; this.userManager = userManager; } //2. public async Task<bool> RegisterUserAsync(RegisterUser register) { bool IsCreated = false; var registerUser = new IdentityUser() { UserName = register.Email, Email = register.Email }; var result = await userManager.CreateAsync(registerUser, register.Password); if (result.Succeeded) { IsCreated = true; } return IsCreated; } //3 public async Task<string> AuthenticateUserAsync(LoginUser inputModel) { string jwtToken = ""; var result = await signInManager.PasswordSignInAsync(inputModel.UserName, inputModel.Password, false, lockoutOnFailure: true); if (result.Succeeded) { var secretKey = Convert.FromBase64String(configuration ["JWTSettings:SecretKey"]); var expiryTimeSpan = Convert.ToInt32(configuration ["JWTSettings:ExpiryInMinuts"]); IdentityUser user = new IdentityUser(inputModel.UserName); var securityTokenDescription = new SecurityTokenDescriptor() { Issuer = null, Audience = null, Subject = new ClaimsIdentity(new List<Claim> { new Claim("username",user.Id,ToString()), }), Expires = DateTime.UtcNow.AddMinutes(expiryTimeSpan), IssuedAt = DateTime.UtcNow, NotBefore = DateTime.UtcNow, SigningCredentials = new SigningCredentials (new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature) }; var jwtHandler = new JwtSecurityTokenHandler(); var jwToken = jwtHandler.CreateJwtSecurityToken(securityTokenDescription); jwtToken = jwtHandler.WriteToken(jwToken); } else { jwtToken = "Login failed"; } return jwtToken; } } }
- The class is constructor injected with IConfiguration interface and SingInManager<IdentityUser> and UserManager<IdentityUser> classes. This will be used to read configurations from appsettings.json, managing users Sign-In and managing users respectively.
- The RegisterUserAsync() method accepts RegisterUser object as input parameter. This method is used to register new user using UserManager class.
- The AuthenticateUserAsync() method accepts the LoginUser object as input parameter. This method is used to sign in the user using SignInManager class. If the user is authenticated successfully, then the JSON Web Token will be generated by reading settings from the appsettings.json file using IConfiuguration object. The SecurityTokenDescriptor class is used to describe token with its Audience, Issuer, Subject, SigningCredentials, etc. properties. The Subject property is used to set the Claims (aka Payload) value that will be stored in token so that the token is validated based on the claim. The SigningCredentials property uses the cryptographic signature and algorithm which will be used to generate token. This information will also used in token validation process. The JwtSecurityTokenHandler class will use the token description to generate the JSON web token. The AuthenticateUserAsync() method will return the token.
using Microsoft.AspNetCore.Mvc; using SecureAPI.Services; using SharedModels.Models; using System.Threading.Tasks; namespace SecureAPI.Controllers { [Route("api/[controller]/[action]")] [ApiController] public class AuthController : ControllerBase { AuthService authenticationService; public AuthController(AuthService authenticationService) { this.authenticationService = authenticationService; } [HttpPost] public async Task<IActionResult> Register(RegisterUser user) { if (ModelState.IsValid) { var IsCreated = await authenticationService.RegisterUserAsync(user); if (IsCreated == false) { return Conflict("The User Already Present"); } var ResponseData = new ResponseData() { Message = $"{user.Email} User Created Successfully" }; return Ok(ResponseData); } return BadRequest(ModelState); } [HttpPost] public async Task<IActionResult> Login(LoginUser inputModel) { if (ModelState.IsValid) { var token = await authenticationService.AuthenticateUserAsync(inputModel); if (token == null) { return Unauthorized("The Authentication Failed"); } var ResponseData = new ResponseData() { Message = token }; return Ok(ResponseData); } return BadRequest(ModelState); } } }
services.AddCors(options=> { options.AddPolicy("cors", policy=> { policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }); }); byte[] secretKey = Convert.FromBase64String (Configuration["JWTSettings:SecretKey"]); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(secretKey), ValidateIssuer = false, ValidateAudience = false }; }); services.AddScoped<AuthService>(); services.AddControllers().AddJsonOptions(options=> { options.JsonSerializerOptions.PropertyNamingPolicy = null; });
app.UseCors("cors"); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization();
[Route("api/[controller]")] [Authorize] [ApiController] public class ProductController : ControllerBase { ProductList products; public ProductController() { products = new ProductList(); } [HttpGet] public IActionResult Get() { return Ok(products); } }
@page "/register" @using SharedModels.Models @using System.Text.Json @inject HttpClient httpClient
<h3>Register New User Component</h3> <div class="container"> <EditForm Model="@user"> <DataAnnotationsValidator> </DataAnnotationsValidator> <ValidationSummary></ValidationSummary> <div class="form-group"> <label>Email</label> <InputText class="form-control" @bind-Value="@user.Email"></InputText> </div> <div class="form-group"> <label>Password</label> <InputText type="password" class="form-control" @bind-Value="@user.Password"> </InputText> </div> <div class="form-group"> <label>Confirm Password</label> <InputText type="password" class="form-control"
@bind-Value="@user.ConfirmPassword"> </InputText> </div> <div class="form-group"> <button class="btn btn-primary" @onclick="@clear">Clear</button> <button class="btn btn-success" @onclick="@register">Register</button> </div> <hr/> <div class="container"> <strong>@responseData.Message</strong> </div> </EditForm> </div>
@code { private RegisterUser user; private ResponseData responseData; protected override Task OnInitializedAsync() { user = new RegisterUser(); responseData = new ResponseData(); return base.OnInitializedAsync(); } void clear() { user = new RegisterUser(); } async Task register() { var response = await httpClient.PostAsJsonAsync ("http://localhost:60626/api/Auth/Register", user); var message = await response.Content.ReadAsStringAsync(); responseData = JsonSerializer.Deserialize<ResponseData>(message); } }
@page "/login" @using SharedModels.Models @using System.Text.Json @inject HttpClient httpClient @inject Blazored.SessionStorage.ISessionStorageService sessionStorage
<h3>Login Component</h3> <div class="container"> <EditForm Model="@user"> <DataAnnotationsValidator> </DataAnnotationsValidator> <ValidationSummary></ValidationSummary> <div class="form-group"> <label>User Name</label> <InputText class="form-control" @bind-Value="@user.UserName"></InputText> </div> <div class="form-group"> <label>Password</label> <InputText type="password" class="form-control" @bind-Value="@user.Password"></InputText> </div> <div class="form-group"> <button class="btn btn-primary" @onclick="@clear">Clear</button> <button class="btn btn-success" @onclick="@login">Login</button> </div> <hr/> <div class="container"> <strong>@responseData.Message</strong> </div> </EditForm> </div>
@code { private LoginUser user; private ResponseData responseData; protected override Task OnInitializedAsync() { user = new LoginUser(); responseData = new ResponseData(); return base.OnInitializedAsync(); } void clear() { user = new LoginUser(); } async Task login() { var response = await httpClient .PostAsJsonAsync("http://localhost:60626/api/Auth/Login", user); var message = await response.Content.ReadAsStringAsync(); responseData = JsonSerializer .Deserialize<ResponseData>(message); //save data in session storage await sessionStorage .SetItemAsStringAsync("token", responseData.Message); } }
@page "/products" @using SharedModels.Models @inject HttpClient httpClient @inject Blazored.SessionStorage.ISessionStorageService sessionStorage
<h3>Products List Component</h3> <div class="container"> <div class="container"> <strong>@message</strong> </div> @if (products.Count == 0 || products == null) { <div class="container"> <strong>No Data is Received</strong> </div> } else { <table class="table-bordered table-striped"> <thead> <tr> <th>Product Id</th> <th>Product Name</th> <th>Price</th> </tr> </thead> <tbody> @foreach(var prd in products) { <tr> <td>@prd.ProductId</td> <td>@prd.ProductName</td> <td>@prd.Price</td> </tr> } </tbody> </table> } </div>
@code { List<Product> products; string message; protected override async Task OnInitializedAsync() { products = new List<Product>(); // check if the token is available then request the API for Access the data string token = await sessionStorage.GetItemAsStringAsync("token"); if (String.IsNullOrEmpty(token)) { message = "The Authentication is not done, please login"; } else { httpClient.DefaultRequestHeaders .Add("Authorization", $"Bearer {token}"); products = await httpClient .GetFromJsonAsync<List<Product>>("http://localhost:60626/api/Product"); } } }
..... builder.Services.AddBlazoredSessionStorage(); ....
<li class="nav-item px-3"> <NavLink class="nav-link" href="register"> <span class="oi oi-list-rich" aria-hidden="true"></span>Register User </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="login"> <span class="oi oi-list-rich" aria-hidden="true"></span> Login </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="products"> <span class="oi oi-list-rich" aria-hidden="true"></span> Get Products </NavLink> </li>
n the browser. In the Blazor project click on the Register User link and enter the information for creating new user as shown in figure 3