###### tags: `Back-end`,`Restful API`,`ASP.NET CORE`,`Aon`,`Security` # Week 05 - Authentication & Authorization ## Introduction This session will cover the essentials of JWT (JSON Web Tokens) for authentication in a .NET Core 8 RESTful API, Will use EF Identity package, diving into how JWT works, its structure, best practices, and how to implement it with middleware and role-based route protection. Authentication is the process of verifying who a user is. In a RESTful API, protecting endpoints ensures only authorized users can access certain data or perform specific actions. ## Identity EF Package and Its Role ASP.NET Core Identity provides the tools to handle authentication and authorization. The Identity framework, when combined with Entity Framework Core (EF Core), offers built-in database support for managing user data. Supports secure password storage, user management, roles, and claims-based authentication. ## Installing and Setting Up ASP.NET Core Identity ### Install the required NuGet packages: `dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore ` ### Extending IdentityUser with Integer Primary Key ```csharp! public class ApplicationUser : IdentityUser<int> { // Additional custom properties can go here } ``` ### Update ApplicationDbContext to use IdentityDbContext: ```csharp! public class MainAppContext : IdentityDbContext<ApplicationUser, IdentityRole<int>, int> { public MainAppContext(DbContextOptions<MainAppContext> options) : base(options) { } } ``` ### Distinct user types (Freelancer, Client, and SystemUser) #### Define a Base User Class with Shared Properties ```csharp public class ApplicationUser : IdentityUser { // Shared properties // // + // Additional shared properties as needed } ``` #### Define Specific User Types Using Inheritance ```csharp public class Freelancer : ApplicationUser { public List<string> Skills { get; set; } // Other Freelancer-specific properties } public class Client : ApplicationUser { public string CompanyName { get; set; } public string ContactInfo { get; set; } // Other Client-specific properties } public class SystemUser : ApplicationUser { public string Department { get; set; } } ``` #### Configure Database Inheritance with EF Core In Entity Framework Core, you can map these classes to a single table in the database (Table-per-Hierarchy or TPH). TPH is efficient and generally recommended when you have similar data structures and don't mind the addition of a Discriminator column. ```csharp protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Configure inheritance builder.Entity<Freelancer>().HasBaseType<ApplicationUser>(); builder.Entity<Client>().HasBaseType<ApplicationUser>(); builder.Entity<SystemUser>().HasBaseType<ApplicationUser>(); // Additional configurations if necessary } ``` #### Configure Identity Since ApplicationUser inherits from IdentityUser, you can use it directly with UserManager<ApplicationUser> and SignInManager<ApplicationUser>. This simplifies the integration with ASP.NET Core Identitys authentication and authorization features. ```csharp builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>() .AddEntityFrameworkStores<MainAppContext>() .AddDefaultTokenProviders(); ``` ### Database Migration and Update Use the following commands to apply migrations and update the database: ```csharp dotnet ef migrations add IdentityMig dotnet ef database update OR Add-Migration IdentityMig Update-Database ``` ## Implementing an Authentication Controller (AuthController) The AuthController will include a Login endpoint to authenticate users by checking their credentials. ```csharp [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly UserManager<ApplicationUser> _userManager; private readonly JwtService _jwtService; public AuthController(UserManager<ApplicationUser> userManager, JwtService jwtService) { _userManager = userManager; _jwtService = jwtService; } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] AuthRequest model) { var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { var role = (await _userManager.GetRolesAsync(user)).FirstOrDefault(); var token = _jwtService.GenerateJWT(user, role); return Ok(new { Token = token }); } return Unauthorized(); } } ``` DTO for Login: ```csharp! public class AuthRequest { public string Username { get; set; } public string Password { get; set; } } ``` ## JWT JWT is a secure, compact token format that’s commonly used to manage authentication between a client and server. Once a user logs in, the server generates a JWT that’s sent to the client. This token, which is self-contained and includes claims about the user (like roles and permissions), enables the client to make authenticated requests without re-authenticating. ### JWT Structure A JWT has three main parts: * Header: Defines the token type and the signing algorithm (e.g., HS256). * Payload: Contains claims (statements about the user, such as username, roles, exp for expiry, etc.). * Signature: Ensures the token's integrity by signing the encoded header and payload with a secret key. The format of a JWT is: `{Header}.{Payload}.{Signature}` ### Install JwtBearer for JWT implementation Install JwtBearer package from Nuget Manager. ### Setup JWT In ConfigureServices, add authentication and configure JWT bearer settings. Make sure to inject UserManager and SignInManager for user authentication. ```csharp public void ConfigureServices(IServiceCollection services) { // Add Identity and configure it services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // JWT Authentication configuration var jwtSettings = Configuration.GetSection("Jwt"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings["Issuer"], ValidAudience = jwtSettings["Audience"], IssuerSigningKey = new SymmetricSecurityKey(key) }; }); services.AddControllers(); } ``` Jwt options setup: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = "yourapi.com", ValidAudience = "yourapi.com", IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("YourSecretKey")) }; }); ``` ### Implementing JwtService ```csharp public class JwtService { private readonly IConfiguration _config; public JwtService(IConfiguration config) { _config = config; } public string GenerateJWT(ApplicationUser user, string role) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: new[] { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), new Claim(ClaimTypes.Role, role) }, expires: DateTime.Now.AddMinutes(Convert.ToDouble(_config["Jwt:ExpireInMinutes"])), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } } ``` Register JwtService in Program.cs: ```jsonld "Jwt": { "Key": "asdkjkejr##$@daslkw[efkf]fadirai", "Issuer": "Titanium", "Audience": "Trainers.Trainees", "ExpireInMinutes": 30 } ``` ### Testing the Login Endpoint Use Postman or a similar tool to send a POST request to /api/auth/login with the correct username and password. If successful, a JWT token will be returned in the response. ### Enable Jwt Middleware to protect the endpoints Middleware for extracting and validating Jwt Token ```csharp! public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); // Add Authentication and Authorization middleware app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } ``` ### Using [Authorize] Attribute to Protect Endpoints Apply [Authorize] on controllers or actions to restrict access to authenticated users: ```csharp [Authorize] [ApiController] [Route("api/[controller]")] public class ProgramsController : ControllerBase { // Only accessible to authenticated users } ``` Checking controller: ```csharp= [Authorize] [Route("api/[controller]")] [ApiController] public class SampleController : ControllerBase { [HttpGet] public IActionResult GetSecureData() { return Ok("This is a secure endpoint"); } } ``` You can specify roles if you need role-based authorization: ```csharp! [Authorize(Roles = "Admin")] public class AdminController : ControllerBase { [HttpGet] public IActionResult GetAdminData() { return Ok("This is an Admin-only endpoint"); } } ``` ## Implementing Role-Based Access Control ```csharp [ApiController] [Route("api/[controller]")] public class RolesController : ControllerBase { private readonly RoleManager<ApplicationRole> _roleManager; public RolesController(RoleManager<ApplicationRole> roleManager) { _roleManager = roleManager; } [HttpPost("create")] public async Task<IActionResult> CreateRole([FromBody] string roleName) { var result = await _roleManager.CreateAsync(new ApplicationRole { Name = roleName }); return result.Succeeded ? Ok() : BadRequest(result.Errors); } [HttpGet("getAll")] public IActionResult GetAllRoles() { var roles = _roleManager.Roles.ToList(); return Ok(roles); } } ``` UsersController to Assign Roles: ```csharp [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly UserManager<ApplicationUser> _userManager; public UsersController(UserManager<ApplicationUser> userManager) { _userManager = userManager; } [HttpPost("assignRole")] public async Task<IActionResult> AssignRole(string userName, string roleName) { var user = await _userManager.FindByNameAsync(userName); if (user == null) return NotFound(); var result = await _userManager.AddToRoleAsync(user, roleName); return result.Succeeded ? Ok() : BadRequest(result.Errors); } } ``` ## Registeration ## Send OTP Verification Code Via WhatsApp Using Twillio ## Bonus In .NET Core 8, you can achieve a unified response format for both successful and error responses by creating a custom response wrapper and implementing global exception handling through middleware. This approach ensures that any unhandled exceptions are caught, and all responses follow the specified structure. Here’s a best-practice implementation: ### Define a Custom Response Wrapper Create a generic wrapper class to encapsulate the response structure. This will standardize responses across your application. ```csharp public class ApiResponse<T> { public bool IsSuccess { get; set; } public T Results { get; set; } public List<ErrorDetail> Errors { get; set; } } public class ErrorDetail { public string Code { get; set; } public string Message { get; set; } } ``` ### Implement Global Exception Handling Middleware ASP.NET Core’s middleware pipeline is ideal for handling exceptions at a global level. By using middleware, you can intercept all unhandled exceptions and format them into your response structure. ```csharp public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { _logger.LogError(ex, "An unhandled exception occurred."); await HandleExceptionAsync(context, ex); } } private Task HandleExceptionAsync(HttpContext context, Exception exception) { context.Response.ContentType = "application/json"; context.Response.StatusCode = StatusCodes.Status500InternalServerError; var response = new ApiResponse<object> { IsSuccess = false, Results = null, Errors = new List<ErrorDetail> { new ErrorDetail { Code = "500", Message = exception.Message } } }; return context.Response.WriteAsJsonAsync(response); } } ``` This middleware catches all exceptions, logs them, and formats the error response according to the specified schema. ### Register the Middleware ```csharp var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // Add middleware app.UseMiddleware<ExceptionHandlingMiddleware>(); // Other configurations app.Run(); ``` ### Create a Helper Method for Consistent API Responses ```csharp public class BaseController : ControllerBase { protected ApiResponse<T> CreateSuccessResponse<T>(T data) { return new ApiResponse<T> { IsSuccess = true, Results = data, Errors = null }; } protected ApiResponse<T> CreateErrorResponse<T>(string code, string message) { return new ApiResponse<T> { IsSuccess = false, Results = default, Errors = new List<ErrorDetail> { new ErrorDetail { Code = code, Message = message } } }; } } ``` ### To Override BadRequest Response cautgh through Validation Define a BaseController class that inherits from ControllerBase. In this class, override the ValidationProblem method to customize how model validation errors are handled. ```csharp using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; public class BaseController : ControllerBase { // Override the default ValidationProblem response to format it in a consistent way public override BadRequestObjectResult ValidationProblem([ActionResultObjectValue] object error = null) { var validationErrors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => new ErrorDetail { Code = "400", // Customize error codes as needed Message = e.ErrorMessage }) .ToList(); var response = new ApiResponse<object> { IsSuccess = false, Results = null, Errors = validationErrors }; return base.BadRequest(response); } } ``` Model validation occurs before the controller action is executed. By default, if the model validation fails, ASP.NET Core automatically returns a 400 Bad Request response without entering the controller action. This behavior is managed by a built-in ModelStateInvalidFilter, which checks ModelState.IsValid and returns a BadRequest response if validation fails. So, when you use [ApiController] on a controller, ASP.NET Core performs model validation automatically, and if the model is invalid, it returns a 400 response before the action code is even reached. Disable the Default Model Validation Response ```csharp builder.Services.AddControllers(options => { options.SuppressModelStateInvalidFilter = true; }); ``` ## Changes Done on the code before below task - `master` branch deleted - `main` branch is the default now. - Unneccessary codes/functions/files deleted. - ApiResponser<T> is updated: - Error become Array of Error object. - Used in all the Http responses - Twilio secrets values moved to `appsettings.json` file. - Example implemented in AuthController illustrates how to access settings and read the values from it. - TPT design for different user types used instead of TPH. - This requires changs in `MainAppContext` `OnModelCreating` function. - When getting list of any type of users, like freelancers or clients in their controllers, new method used to filter the type, function is `ofType<T>` - OTP generation function added in `Utilities` directory to generate 6 digit code. - For clean code: - avoid hard coded directly and use constants, Constants class added - In registeration method/endpoint, UserType added as input, to check which type of user is trying to register. - Api classified and versioned: - we classified apis to be under `mobile`, this indicates that endpoints designed to mobile front-end devs only. - in each endpoint, v1 added to specificy that this endpoints implemented under v1 logic. - According to the above, we re-organized controllers of mobile and v1 in the project structure. - `appsettings.json` added in `.gitignore`, removed from the Git tracking and commit history for security reasons - `appsettings.example.json` added for you without any secret values, just to follow the same definition. ## Capstone Project Task - Week 05 ### Understanding - We will implement primary endpoints for the capstone project "Aon Freelance" - This task focuses on implement endpoints: - Registeration endpoint for freelancer and clients - Login endpoint for freelancer and client - Vertify OTP endpoint - Load user profile - For registeration, OTP verification will implement via using Twillo and WhatsApp - For login, we will implement JWT token generation to send it back after Login in the response. ### Design - EF Identity core package to be used for User and Sign in management. - JwtBearer package to be used for managing implementation of Authorization - TPT design will be followed: - User entity will be used as base table will contain all the shared properties ### Requirements 1. Complete method/endoint of Login: - Return User Details and Jwt Access Token in the response using `ApiResponse`, final json schema should be as below: ```jsonld { IsSuccess:<boolean>, Results:{ UserDetails:{ }, AccessToken:`JWT_HERE` } } ``` Hints: - Install JwtBearer, config it and write the Jwt implementation in a seperate class for clean code, you can follow DI and Scoped service for this purpose. - Customize response of UserDetails by using DTO concept. - Jwt configuration should not be hard coded like Key, instead, put them in you `appsettings.json` 2. Implement OTP Management: - Create OTP entity class, to be reflected in DB as `otps` - Define DbSet of OTP in MainAppContext. - OTP class/table should have: - PhoneNumber - Code(6 digits) - CreatedAt (DateTime, date of record insertion) - ExpireAt (DateTime, date of expiration, 5 - 10 minutes) - IsUsed (Boolean to indicate if this sent OTP used or not) - Go to Register method/endpoint, and implement inserting of generated OTP into this table. - Go to `verify` endpoint/method and handle logic of: - Expiration ( if the OTP sent is xpired, then reject verification) - IsUsed (if OTP is valid and not expired but used previsouly!) 3. Implement new endpoints with below specifications: - Route name is: `api/mobile/v1/users/{id}/profile` - Description: This endpoint used for returning Profile of a user by giving Id, profile information is the same stored in Users table + Props of the User type, example if user type is Freelancer, then return props from User + Skills, and the same thing for Client. - Http Verb is `GET` with passing param id, id is ID of the user that we want to return it's details - Logic: - Check if user exists by sent Id - Return 404 if not found, using ApiResponse schema. - if exist, check its type and return properties of User + props of the user type. Hints: - Create controller of Users in the right place in the project strcuture. - Verioning the endpoint - Use UserManager from IDentity package to find the user by its id, or access the Users through `_mainAppContext`. - You may need to prepate UserResponseDTO to represent the schema that should be returned in ApiResponse, check if exist class if enought or not before creating new one. ### Steps to follow to start resolve the requirements: 1. Clone or pull branch `main` from the synced repo. 2. Create new branch, name it `YOUR_NAME/aon-project` example: `fadiramzi/aon-project` Code to execute: `git checkout -b fadiramzi/aon-project` 2. Now, Clone `appsettings.example.json`, and put your secrets of Twilio, don't put secrets in `appsettings.example.json` ---- ### General Hints: - Avoid hard coded - Avoid write secrets in the code directly, use `appsettings.json` instead. - Don't shared your secrets with your friend :) - Follow the same changes that applied to the repo, like: - ApiResponse - Define constants in Constants class - Project Structures in creating new files. - Follow the convention name. - Always use DTOs to represent your inputs and responses schema. - In OTPManager class, there generate function ready to be used by you. - Feel Free to be creative in writing your changes, like adding new files/classes and further functions. -