π οΈ Task Checklist
π§ Branching
- Create the
feature/[name]
branch fromdevelop
Task
- Task
π Suggested Folder:
Domain/Framework/Interfaces/Respositories
π§ Hints & Notes
π Acknowledgements
- ChatGPT for snippet refinement and explanations
π References
This file
Add a field in Database
- Add
User
field inRoles
table using SSMS or Seed data in DbContext file - Make sure the property
PersonId
is nullable inAccount
, so you can add fields related to βPersonβ later after registration
Branching
- Create the feature/authentication branch based on develop
Adjusting Account and Configurations
- Add navigation property for
AccountRoles
in Account
public virtual ICollection<AccountRole> AccountRoles { get; set; }
- Add navigation properties for Account and Role in
AccountRole
public virtual Role Role { get; set; }
public virtual Account Account{ get; set; }
- Update the entity configuration to reflect relationship mappings:
builder.HasOne<Account>(ar => ar.Account)
.WithMany(a => a.AccountRoles)
.HasForeignKey(ar => ar.AccountId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<Role>(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:
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<string> Roles { get; set; }
}
AuthResponseDto
- Define a DTO for authentication responses:
public class AuthResponseDto
{
public long Id { get; set; }
public string Token { get; set; } = null!;
public string PhoneNumber { get; set; } = null!;
public List<string> Roles { get; set; }
}
- Define a DTO for login requests:
public class LoginRequestDto
{
public string PhoneNumber { get; set; } = null!;
public string Password { get; set; } = null!;
}
RegisterRequestDto
- Define a DTO for registration with validation attributes:
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:
[ApiController]
public class AuthController : ControllerBase
Then this:
[HttpPost("register")]
public async Task<IActionResult> 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:
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 ifConfirmPassword
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:
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:
{
"phoneNumber": "",
"password": "123",
"confirmPassword": "abc"
}
The backend will automatically return:
{
"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:
CreateMap<Account, AccountDto>()
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.AccountRoles.Select(x=>x.Role.Title)));
CreateMap<AccountDto, Account>()
.ForMember(dest => dest.AccountRoles, opt => opt.Ignore());
Add Password Hasher Utility
- Create a password hashing utility class
π Suggested Folder: ApplicationLayer/Utils/
PasswordHasher.cs
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
Task<Account> GetByPhoneNumberAsync(string phoneNumber);
Task AddAccountRoleAsync(AccountRole accountRole);
- Implement the methods in
AccountRepository
public async Task AddAccountRoleAsync(AccountRole accountRole)
{
await DbContext.AccountRoles.AddAsync(accountRole);
}
public async Task<Account> 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:
public static Result<T> Error(T data, string errorMessage) => new() { Status = ResultStatus.Error, Data = data, ErrorMessage = errorMessage };
Creating IAuthService.cs
and AuthService.cs
- Define the
IAuthService
interface
public interface IAuthService
{
Task<Result<AuthResponseDto>> RegisterAsync(RegisterRequestDto request);
Task<Result<AuthResponseDto>> 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
//...
builder.Services.AddScoped<IAuthService, AuthService>();
//...
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
"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
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
string GenerateToken(AuthResponseDto authResponseDto);
- Create
JwtGenerator
, implementingIJwtGenerator
π 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
builder.Services.AddScoped<IJwtGenerator, JwtGenerator>();
- Bind
JwtSettings
from configuration
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
- Configure JWT authentication
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>();
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
beforeapp.UseAuthorization
app.UseAuthentication();
app.UseAuthorization();
Add ApiControllers
-
Create
AuthController
π Suggested Folder: WebApi/Controllers/AuthController.cs
-
Add Class and Constructor
private readonly IAuthService _authService;
private readonly IJwtGenerator _jwtGenerator;
public AuthController(IAuthService authService, IJwtGenerator jwtGenerator)
{
_authService = authService;
_jwtGenerator = jwtGenerator;
}
- Add Register Method
public async Task<IActionResult> 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
[HttpPost("login")]
public async Task<IActionResult> 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
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