The main objectives of this article are:
Let's implement the 'LoginAsync' method in the 'UserService'.
- Generating JWT Access Token
- Creating Login API Endpoint.
- Creating The Login Form In Blazor WebAssembly Application
Create 'LoginVm' As A Form Model In The BlazorWasm Application:
Let's create a 'LoginVm' as a form model in the Blazor WebAssembly application.
BlazorWasm_Project/ViewModels/Accounts/LoginVm.cs:
namespace JWT.Auth.BlazorUI.ViewModels.Account { public class LoginVm { public string Email { get; set; } public string Password { get; set; } } }
Create 'LoginValidationVm' Validator For 'LoginVm':
Let's create the validation model for 'LoginVm' like 'LoginValidationVm' in the Blazor application.
BlazorWasm_Project/ViewModels/Accounts/LoginValidationVm:
using FluentValidation; namespace JWT.Auth.BlazorUI.ViewModels.Account { public class LoginValidationVm:AbstractValidator<LoginVm> { public LoginValidationVm() { RuleFor(_ => _.Email).NotEmpty() .EmailAddress() .WithMessage("Invalid email"); RuleFor(_ => _.Password).NotEmpty().WithMessage("Your password cannot be empty") .MaximumLength(16).WithMessage("Invalid Password") .Matches(@"[A-Z]+").WithMessage("Invalid Password") .Matches(@"[a-z]+").WithMessage("Invalid Password") .Matches(@"[0-9]+").WithMessage("Invalid Password") .Matches(@"[\@\!\?\*\.]+").WithMessage("Invalid Password"); } public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) => { var result = await ValidateAsync(ValidationContext<LoginVm>.CreateWithOptions((LoginVm)model, x => x.IncludeProperties(propertyName))); if (result.IsValid) return Array.Empty<string>(); return result.Errors.Select(e => e.ErrorMessage); }; } }
- Here added 'Email' & 'Password' validation rules. Here 'ValidateValue' Func returns the validation error message, this function will be used by the 'Login' form.
Create A 'Login.razor' Component:
Let's create a new blazor component like 'Login.razor' in the 'Pages/Accounts' folder.
BlazorWasm/Pages/Accounts/Login.razor:(HTML Part)
@page "/login" <div class="ma-6 d-flex justify-center"> <MudChip Color="Color.Primary"> <h3>Login Form</h3> </MudChip> </div> <div class="ma-6 d-flex justify-center"> <MudCard Width="500px"> <MudForm Model="loginModel" @ref="form" Validation="loginValidation.ValidateValue"> <MudCardContent> <MudTextField @bind-Value="loginModel.Email" For="@(() => loginModel.Email)" Immediate="true" Label="Email" /> <MudTextField @bind-Value="loginModel.Password" For="@(() => loginModel.Password)" Immediate="true" Label="Password" InputType="InputType.Password" /> <MudCardActions> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="LoginAsync">Login</MudButton> </MudCardActions> </MudCardContent> </MudForm> </MudCard> </div>
- (Line: 9) Here 'Model' attribute is assigned with the 'loginModel' variable of type 'LoginVm'. Here '@ref' gives control over the form. The 'Validation' attribute is assigned with 'loginValidation.ValidateValue'.
- (Line: 21) Here 'Login' button click registered with the 'LoginAsync' method.
@code { LoginVm loginModel = new LoginVm(); LoginValidationVm loginValidation = new LoginValidationVm(); MudForm form; private async Task LoginAsync() { await form.Validate(); if (form.IsValid) { // Invoke Login API call. } } }
- (Line: 2) Initialized the 'LoginVm' form model
- (Line: 4) Initialized the 'LoginValidationVm' validation model.
- (Line: 8-15) The 'LoginAsync' method validates form data and then invokes a login API call.
BlazorWasm/Shared/MainLayout.razor:
<MudLink Underline="Underline.None" Color="Color.Inherit" Href="/login" Class="mr-2">Login</MudLink>
Configure Required Token Settings In JSON File In API Project:
Let's add the following token settings in 'appsettings.Development.json' in the API project.
API_Project/appsettings.Development.json:
"TokenSettings": { "SecretKey": "yx9UH7K3CPUcQ2xsB8KjEb1vxvnSA1GZhRXbRePN", "Issuer": "localhost:7045", "Audience": "localhost:7256" }
- The 'SecretKey' value is some sort of security key. Any random value can be used as this 'SecretKey'.
- The 'Issuer' is like the identification of the server that generates the token. In the JWT access token, we will have claims like 'iss' which is an application claim. So 'Issuer' value will be the API domain.
- The 'Audience' is like the client application which receives our token. It makes sure that a valid client application requests our JWT endpoint. In JWT we will have claims like 'aud'. So the 'Audience' value will be the domain name of the client application.
API_Project/Shared/Settings/TokenSettings.cs:
namespace JWT.Auth.API.Shared.Settings { public class TokenSettings { public string SecretKey { get;set; } public string Issuer { get; set; } public string Audience { get; set; } } }Register the token settings in the 'Program.cs'.
API_Project/Program.cs:
builder.Services.Configure<TokenSettings>(builder.Configuration.GetSection("TokenSettings"));
Create Login Endpoint Response Entity Like 'JWTTokenResponseDto':
Let's create the login endpoint response entity like 'JWTTokeResponseDto' in the 'Dtos' folder.
API_Project/Dtos/JWTTokenResponseDto.cs:
namespace JWT.Auth.API.Dtos { public class JWTTokenResponseDto { public string AccessToken { get; set; } } }
Create Login Payload Entity Like 'LoginDto':
Let's create the login payload entity like 'LoginDto' in the 'Dtos' folder.
API_Project/Dtos/LoginDto.cs:
namespace JWT.Auth.API.Dtos { public class LoginDto { public string Email { get; set; } public string Password { get; set; } } }
Generate JWT(JSON Web Token) Access Token:
In 'IUserService' add a method definition like 'LoginAsync'.
API_Project/Services/IUserService.cs:
Task<(bool IsLoginSucess, JWTTokenResponseDto TokenResponse)> LoginAsync(LoginDto loginpayload);
Inject the 'TokenSettings' into the 'UserService' constructor.
API_Project/Services/UserService.cs:
public class UserService : IUserService { private readonly MyWorldDbContext _dbContext; private readonly TokenSettings _tokenSettings; public UserService(MyWorldDbContext dbContext, IOptions<TokenSettings> tokenSettings) { _dbContext = dbContext; _tokenSettings = tokenSettings.Value; } }Now add logic for user login password validation in 'UserService'.
API_Project/Services/UserService.cs:
private bool PasswordVerification(string plainPassword, string dbPassword) { byte[] dbPasswordHash = Convert.FromBase64String(dbPassword); byte[] salt = new byte[16]; Array.Copy(dbPasswordHash, 0, salt, 0, 16); var rfcPassowrd = new Rfc2898DeriveBytes(plainPassword, salt, 1000, HashAlgorithmName.SHA1); byte[] rfcPasswordHash = rfcPassowrd.GetBytes(20); for (int i = 0; i < rfcPasswordHash.Length; i++) { if (dbPasswordHash[i + 16] != rfcPasswordHash[i]) { return false; } } return true; }
- Here 'PasswordVerification' is a private method it has input parameters like user login payload plain password and database password(hashed user password in the database).
- (Line: 3) Extract byte array data from the database password and assigned it to the 'dbPasswordHash' variable.
- (Lines: 5&6) In the 'dbPasswordHash' variable first 16 bytes are the salt keys for the password hash. So read the salt key data and assign it to the 'salt' byte array variable.
- (Lines: 8&9) So here we hash our plain password using the salt key with help of the 'Rfc2898DeriveBytes' instance.
- (Line: 11-18) Compare each byte of our database password and the plain password, if they match then the password provided by the login form is valid else invalid password.
API_Project/Services/UserService.cs:
private string GenerateJwtAccessToken(User user) { var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.SecretKey)); var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var claims = new List<Claim>(); claims.Add(new Claim("Sub", user.Id.ToString())); claims.Add(new Claim("FirstName", user?.FirstName ?? string.Empty)); claims.Add(new Claim("LastName", user?.LastName ?? string.Empty)); claims.Add(new Claim("Email", user?.Email ?? string.Empty)); var securityToken = new JwtSecurityToken( issuer: _tokenSettings.Issuer, audience: _tokenSettings.Audience, expires: DateTime.Now.AddMinutes(30), signingCredentials: credentials, claims: claims); return new JwtSecurityTokenHandler().WriteToken(securityToken); }
- Here 'GenerateJWTAccessToken' private method to generate the JWT access token.
- (Line: 3) Generate the 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey' instance using our 'SecretKey' value.
- (Line: 5) Generate the 'Microsoft.IdentityModel.Tokens.SigninCredentials' instance using 'SymmetricSecurityKey' & 'SecurityAlgorithm.HmacSha256'.
- (Line: 7-11) Creating user claims that we will store JWT access tokens.
- (Line: 13-18) Generates the 'System.IdntityModel.Tokens.Jwt.JwtSecurityToken' with input parameters like 'Issuer', 'Audience', 'Expiration'(token expiration), 'SignInCredentials', and 'Claims'.
- (Line: 20) Return the user's JWT access token.
API_Project/Services/UserService.cs:
public async Task<(bool IsLoginSucess, JWTTokenResponseDto TokenResponse)> LoginAsync(LoginDto loginpayload) { if (string.IsNullOrEmpty(loginpayload.Email) || string.IsNullOrEmpty(loginpayload.Password)) { return (false, null); } var user = await _dbContext.User.Where(_ => _.Email.ToLower() == loginpayload.Email.ToLower()) .FirstOrDefaultAsync(); if (user == null) { return (false, null); } bool validUserPassowrd = PasswordVerification(loginpayload.Password, user.Password); if (!validUserPassowrd) { return (false, null); } string jwtAccessToken = GenerateJwtAccessToken(user); var result = new JWTTokenResponseDto { AccessToken = jwtAccessToken, }; return (true, result); }
- (Line: 3-7) Checking user payload is not empty.
- (Lines: 9&10) Query the database with the user's email address.
- (Line: 12) Checking whether user data exists or not.
- (Lines: 14&15) Checking whether the user password is valid or not.
- (Line: 17-22) Generate the JWT access token.
Create An User Login Endpoint:
Let's add a user login endpoint in our 'UserController'.
API_Project/Controllers/UserController.cs:
[HttpPost("login")] public async Task<IActionResult> LoginAsync(LoginDto loginPayload) { var result = await _userService.LoginAsync(loginPayload); if (result.IsLoginSucess) { return Ok(result.TokenResponse); } ModelState.AddModelError("LoginError", "Invalid email or password"); return BadRequest(ModelState); }
Invoke Login API From Blazor WebAssembly Login Form:
Let's update the logic in the 'Login.razor' component to invoke the login API call.
BlazorWasm/Pages/Account/Login.razor:(HTM Part)
@page "/login" @using System.Text.Json; @using System.Text; @inject HttpClient _http <div class="ma-6 d-flex justify-center"> <MudChip Color="Color.Primary"> <h3>Login Form</h3> </MudChip> </div> <div class="ma-6 d-flex justify-center"> <MudCard Width="500px"> <MudForm Model="loginModel" @ref="form" Validation="loginValidation.ValidateValue"> <MudCardContent> @if (!string.IsNullOrEmpty(APIErrorMessage)) { <MudChip Class="d-flex justify-center" Color="Color.Error"> <h3>@APIErrorMessage</h3> </MudChip> } <MudTextField @bind-Value="loginModel.Email" For="@(() => loginModel.Email)" Immediate="true" Label="Email" /> <MudTextField @bind-Value="loginModel.Password" For="@(() => loginModel.Password)" Immediate="true" Label="Password" InputType="InputType.Password" /> <MudCardActions> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="LoginAsync">Login</MudButton> </MudCardActions> </MudCardContent> </MudForm> </MudCard> </div>
- (Line: 4) Inject the 'HttpClient' instance
- (Line: 15-20) Rendering the API response.
@code { LoginVm loginModel = new LoginVm(); LoginValidationVm loginValidation = new LoginValidationVm(); MudForm form; string APIErrorMessage = string.Empty; private async Task LoginAsync() { await form.Validate(); if (form.IsValid) { var jsonPayload = JsonSerializer.Serialize(loginModel); var requestContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var response = await _http.PostAsync("/api/User/login", requestContent); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errors = await response.Content .ReadFromJsonAsync<Dictionary<string, List<string>>>(); if (errors.Count > 0) { foreach (var item in errors) { foreach (var errorMessage in item.Value) { APIErrorMessage = $"{errorMessage} | "; } } } } else if (response.StatusCode == System.Net.HttpStatusCode.OK) { // On successful user authentication. } else { APIErrorMessage = "Unable to do login, please try later"; } } } }
- (Line: 8) Declared variable like 'ApiErrorMessage'.
- (Line: 10-45) Here invoking the user login post API call. If the API returns BadRequest or any other error value to the 'APIErrorMessage' variable. If the API return success then we receive the JWT access token as response.
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on the.NET7 Blazor WebAssembly JWT Authentication. using I love to have your feedback, suggestions, and better techniques in the comment section below.
Refer:
Part-1 |Blazor WebAssembly[.NET 7] JWT Authentication Series | User Registration
Part-3 |Blazor WebAssembly[.NET 7] JWT Authentication Series | Implement Blazor AuthenticationStateProvider & Invoke Secure Endpoint Using JWT Access Token
Part-3 |Blazor WebAssembly[.NET 7] JWT Authentication Series | Implement Blazor AuthenticationStateProvider & Invoke Secure Endpoint Using JWT Access Token
Is there any link to download a working code base?
ReplyDelete