###### 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.
-