# Add a field in Database - [ ] Add `User` field in `Roles` table using SSMS or Seed data in DbContext file - [ ] Make sure the property `PersonId` is nullable in `Account`, so you can add fields related to "Person" later after registeration # Branching - [ ] Create the feature/authentication branch based on develop # Adjusting Account and Configurations - [ ] Add navigation property for `AccountRoles` in Account ```c# public virtual ICollection AccountRoles { get; set; } ``` - [ ] Add navigation properties for Account and Role in `AccountRole` ```C# public virtual Role Role { get; set; } public virtual Account Account{ get; set; } ``` - [ ] Update the entity configuration to reflect relationship mappings: ```c# builder.HasOne(ar => ar.Account) .WithMany(a => a.AccountRoles) .HasForeignKey(ar => ar.AccountId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(ar => ar.Role) .WithMany() .HasForeignKey(ar => ar.RoleId) .OnDelete(DeleteBehavior.Restrict); ``` # Creating DTOs ๐Ÿ“‚ Suggested Folder: ApplicationLayer/DTOs/[RelatedFolder] ## AccountDto - [ ] Create `AccountDto` to expose relevant account information: ```c# public class AccountDto { public long Id { get; set; } public required string PhoneNumber { get; set; } public required string Password { set; get; } public string? Email { get; set; } public long? PersonId { get; set; } public List Roles { get; set; } } ``` ## AuthResponseDto - [ ] Define a DTO for authentication responses: ```c# public class AuthResponseDto { public long Id { get; set; } public string Token { get; set; } = null!; public string PhoneNumber { get; set; } = null!; public List Roles { get; set; } } ``` - [ ] Define a DTO for login requests: ```c# public class LoginRequestDto { public string PhoneNumber { get; set; } = null!; public string Password { get; set; } = null!; } ``` ## RegisterRequestDto - [ ] Define a DTO for registration with validation attributes: ```c# public class RegisterRequestDto { [Required(ErrorMessage = "Phone number is required.")] [Phone(ErrorMessage = "Phone number format is invalid.")] public required string PhoneNumber { get; set; } [Required(ErrorMessage = "Password is required.")] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] public required string Password { get; set; } [Compare("Password", ErrorMessage = "Passwords do not match.")] public required string ConfirmPassword { get; set; } } ``` ### ๐Ÿ”น **1. What do the annotations like `[Required]`, `[Phone]`, `[MinLength]`, `[Compare]` on the DTO do?** These are **Data Annotations** from `System.ComponentModel.DataAnnotations`. Theyโ€™re used by: - The ASP.NET Core `[ApiController]` attribute - **Model binding & automatic validation** **What happens:** If your controller is marked with `[ApiController]`, ASP.NET Core will **automatically validate** the DTO against these annotations **before entering your action method**. Example: ```csharp [ApiController] public class AuthController : ControllerBase ``` Then this: ```csharp [HttpPost("register")] public async Task Register(RegisterRequestDto dto) ``` If `dto.PhoneNumber` is missing, it **wonโ€™t even run your logic**, and will return a `400 Bad Request` with validation errors. > โ€œWhy are these here if my frontend is separate?โ€ โœ… **Answer**: They're still useful: - For **security and safety**: you _must_ validate on the backend โ€” never trust the frontend. - For **auto validation** before hitting your logic โ€” saving you boilerplate checks. - You can use them for **Swagger/OpenAPI documentation** as well. Frontend validation is for **user experience**, not security. --- ### ๐Ÿ”น **2. Where should password requirements be checked? Frontend or backend?** โœ… **Both.** - **Frontend**: show real-time UX feedback (โ€œPassword must be 6+ charactersโ€). - **Backend**: enforce security. **Backend is the source of truth.** Frontend can be bypassed (e.g., Postman). In the backend, you can either: - Use annotations like `[MinLength(6)]` - Or do manual checks: ```csharp if (dto.Password.Length < 6) return BadRequest("Password must be at least 6 characters long."); ``` --- ### ๐Ÿ”น **3. Should confirm password be in the backend?** โœ… **Yes โ€” if you're doing password comparison in backend.** - `[Compare("Password")]` will validate if `ConfirmPassword` matches. - Otherwise, youโ€™ll need to check manually. You **can skip sending ConfirmPassword to backend** and just validate in frontend if youโ€™re confident your frontend handles it. But again: if someone sends malformed input manually (e.g., via Postman), backend should defend. ๐Ÿ’ก **Best practice:** - Validate `ConfirmPassword` in frontend (UX) - Do one last check in backend, or use `[Compare]` for auto-validation --- ### ๐Ÿ”น **4. Is it OK to send plain password in request? Or should we hash it on frontend?** **โœ… YES โ€” it is OK and standard to send raw password in the login/signup request.** Why? - Passwords are sent over **HTTPS**, which encrypts the entire request. - Hashing on frontend is _not_ secure, because: - Your algorithm/salt would be exposed - It defeats the purpose of salting and hashing correctly - You lose control over security management ### ๐Ÿ”น **5. Error Response from Automatic Model Validation** If your DTO looks like this: ```csharp public class RegisterRequestDto { [Required(ErrorMessage = "Phone number is required.")] [Phone(ErrorMessage = "Phone number format is invalid.")] public string PhoneNumber { get; set; } [Required(ErrorMessage = "Password is required.")] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] public string Password { get; set; } [Compare("Password", ErrorMessage = "Passwords do not match.")] public string ConfirmPassword { get; set; } } ``` And the frontend sends this: ```json { "phoneNumber": "", "password": "123", "confirmPassword": "abc" } ``` #### The backend will automatically return: ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "PhoneNumber": [ "Phone number is required." ], "Password": [ "Password must be at least 6 characters long." ], "ConfirmPassword": [ "Passwords do not match." ] } } ``` This is thanks to `[ApiController]` on your controller class. The framework uses the **ModelState** and returns errors in a structured way. --- ## Adding Mappings - [ ] Update `MappingProfile` with the following mappings: ```c# CreateMap() .ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.AccountRoles.Select(x=>x.Role.Title))); CreateMap() .ForMember(dest => dest.AccountRoles, opt => opt.Ignore()); ``` # Add Password Hasher Utility - [ ] Create a password hashing utility class ๐Ÿ“‚ Suggested Folder: ApplicationLayer/Utils/`PasswordHasher.cs` ```c# public static class PasswordHasher { public static string HashPassword(string password) { byte[] salt = RandomNumberGenerator.GetBytes(16); var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256); byte[] hash = pbkdf2.GetBytes(32); byte[] hashBytes = new byte[48]; Array.Copy(salt, 0, hashBytes, 0, 16); Array.Copy(hash, 0, hashBytes, 16, 32); return Convert.ToBase64String(hashBytes); } public static bool VerifyPassword(string password, string hashedPassword) { byte[] hashBytes = Convert.FromBase64String(hashedPassword); byte[] salt = new byte[16]; Array.Copy(hashBytes, 0, salt, 0, 16); var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256); byte[] hash = pbkdf2.GetBytes(32); for (int i = 0; i < 32; i++) { if (hashBytes[i + 16] != hash[i]) return false; } return true; } } ``` # Modifying Account Repository - [ ] Add the following methods in **`IAccountRepository`** ```c# Task GetByPhoneNumberAsync(string phoneNumber); Task AddAccountRoleAsync(AccountRole accountRole); ``` - [ ] Implement the methods in **`AccountRepository`** ```c# public async Task AddAccountRoleAsync(AccountRole accountRole) { await DbContext.AccountRoles.AddAsync(accountRole); } public async Task GetByPhoneNumberAsync(string phoneNumber) { var user = await DbContext.Accounts.Include(x => x.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber); return user; } ``` # Creating Service ## Fix `Result.cs` Error Method - [ ] Update the `Error` method to include error messages: ```c# public static Result Error(T data, string errorMessage) => new() { Status = ResultStatus.Error, Data = data, ErrorMessage = errorMessage }; ``` ## Creating `IAuthService.cs` and `AuthService.cs` - [ ] Define the `IAuthService` interface ```c# public interface IAuthService { Task> RegisterAsync(RegisterRequestDto request); Task> LoginAsync(LoginRequestDto request); } ``` - [ ] Implement the interface in `AuthService.cs` Use this project as a reference: https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/Services/AuthService.cs ## Register `IAuthService` in Service in `Program.cs` - [ ] Add to `Program.cs` ```c# //... builder.Services.AddScoped(); //... ``` # Adding JWT ## Installing Required NuGet Packages Install these packages in the **`WebApi` (Presentation Layer)** project: ``` Microsoft.AspNetCore.Authentication.JwtBearer Microsoft.IdentityModel.Tokens System.IdentityModel.Tokens.Jwt ``` ## Create JWT Configuration Classes - [ ] Add JWT section to `appsettings.json` ```xaml "Jwt": { "Key": "[supersecretkeyyoustoresecurely]", "Issuer": "[Issuer]", "Audience": "MyAppUsers", "ExpiryMinutes": 60 } ``` Note that you should fill the values as you wish - these are just samples - [ ] Create `JwtSettings` and add the following method ๐Ÿ“‚ Suggested Folder: WebAPI/Authentication ```c# public class JwtSettings { public string Key { get; set; } = null!; public string Issuer { get; set; } = null!; public string Audience { get; set; } = null!; public int ExpiryMinutes { get; set; } } ``` - [ ] Create `IJwtGenerator` and add the following method ๐Ÿ“‚ Suggested Folder: WebAPI/Authentication ```c# string GenerateToken(AuthResponseDto authResponseDto); ``` - [ ] Create `JwtGenerator`, implementing `IJwtGenerator` ๐Ÿ“‚ Suggested Folder: WebAPI/Authentication use this project as a reference https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.WebAPI/Authentication/JwtGenerator.cs ## Configuring Jwt in Program.cs - [ ] Register `JwtGenerator` service ```c# builder.Services.AddScoped(); ``` - [ ] Bind `JwtSettings` from configuration ```c# builder.Services.Configure(builder.Configuration.GetSection("Jwt")); ``` - [ ] Configure JWT authentication ```c# var jwtSettings = builder.Configuration.GetSection("Jwt").Get(); builder.Services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtSettings.Issuer, ValidateAudience = true, ValidAudience = jwtSettings.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)), ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; }); builder.Services.AddAuthorization(); ``` - [ ] Add `app.UseAuthentication` before `app.UseAuthorization` ```c# app.UseAuthentication(); app.UseAuthorization(); ``` # Add ApiControllers - [ ] Create `AuthController` ๐Ÿ“‚ Suggested Folder: WebApi/Controllers/`AuthController.cs` - [ ] Add Class and Constructor ```c# private readonly IAuthService _authService; private readonly IJwtGenerator _jwtGenerator; public AuthController(IAuthService authService, IJwtGenerator jwtGenerator) { _authService = authService; _jwtGenerator = jwtGenerator; } ``` - [ ] Add Register Method ```c# public async Task Register(RegisterRequestDto request) { var result = await _authService.RegisterAsync(request); if (!result.IsSuccess) return BadRequest(result.ErrorMessage); var token = _jwtGenerator.GenerateToken(result.Data); var response = new AuthResponseDto { PhoneNumber = result.Data.PhoneNumber, Roles = result.Data.Roles, Token = token }; return Ok(response); } ``` - [ ] Add Login Method ```c# [HttpPost("login")] public async Task Login(LoginRequestDto request) { var result = await _authService.LoginAsync(request); if (!result.IsSuccess) return Unauthorized(result.ErrorMessage); var token = _jwtGenerator.GenerateToken(result.Data); var response = new AuthResponseDto { PhoneNumber = result.Data.PhoneNumber, Roles = result.Data.Roles, Token = token }; return Ok(response); } ``` - [ ] Create AccountController ๐Ÿ“‚ Suggested Folder: WebApi/Controllers/AccountController.cs ```c# public class AccountController : ControllerBase { [Authorize(Roles = "User")] [HttpGet("profile")] public IActionResult GetProfile() { return Ok("Hi there, hello"); } } ``` # Merge - [ ] Create a PR and merge the current branch with develop