Skip to content

Commit f1c6466

Browse files
feat(auth): implement secure authentication flow with CORS and password hashing
- Add CORS configuration for frontend access in development and production - Replace plain password storage with hashing using IPinHasher service - Update LoginCommandHandler to use IRequestApiResponseHandler and return proper API responses - Standardize error response codes in ApiResponse class
1 parent dce4723 commit f1c6466

File tree

7 files changed

+112
-20
lines changed

7 files changed

+112
-20
lines changed

Common/Responses.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ public ApiResponse(string message, int code = 0, int httpStatus = 400)
3333
public static ApiResponse<T> Success(T data, string message = "Success")
3434
=> new(data, message, 0, 200);
3535

36-
public static ApiResponse<T> NotFound(string message = "Not Found", int code = 40401)
36+
public static ApiResponse<T> NotFound(string message = "Not Found", int code = 401)
3737
=> new(message, code, 404);
3838

39-
public static ApiResponse<T> BadRequest(string message = "Bad Request", int code = 40001)
39+
public static ApiResponse<T> BadRequest(string message = "Bad Request", int code = 400)
4040
=> new(message, code, 400);
4141

42-
public static ApiResponse<T> Unauthorized(string message = "Unauthorized", int code = 40101)
42+
public static ApiResponse<T> Unauthorized(string message = "Unauthorized", int code = 401)
4343
=> new(message, code, 401);
4444

45-
public static ApiResponse<T> Forbidden(string message = "Forbidden", int code = 40301)
45+
public static ApiResponse<T> Forbidden(string message = "Forbidden", int code = 403)
4646
=> new(message, code, 403);
4747

48-
public static ApiResponse<T> Conflict(string message = "Conflict", int code = 40901)
48+
public static ApiResponse<T> Conflict(string message = "Conflict", int code = 409)
4949
=> new(message, code, 409);
5050

51-
public static ApiResponse<T> InternalServerError(string message = "Internal Server Error", int code = 50001)
51+
public static ApiResponse<T> InternalServerError(string message = "Internal Server Error", int code = 500)
5252
=> new(message, code, 500);
5353
}

Features/Auth/Commands/LoginCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
using MediatR;
2+
using PaymentCoreServiceApi.Common.Mediator;
23

34
namespace PaymentCoreServiceApi.Features.Auth.Commands;
45

5-
public record LoginCommand : IRequest<LoginResponse>
6+
public record LoginCommand : IRequestApiResponse<LoginResponse>
67
{
78
public string UserName { get; init; } = default!;
89
public string Password { get; init; } = default!;
Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
using MediatR;
2+
using Microsoft.AspNetCore.Identity;
23
using Microsoft.EntityFrameworkCore;
3-
using PaymentCoreServiceApi.Core.Interfaces.Repositories.Write;
4+
using PaymentCoreServiceApi.Common;
5+
using PaymentCoreServiceApi.Common.Mediator;
46
using PaymentCoreServiceApi.Infrastructure.DbContexts;
57
using PaymentCoreServiceApi.Services;
68

79
namespace PaymentCoreServiceApi.Features.Auth.Commands;
810

9-
public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponse>
11+
public class LoginCommandHandler : IRequestApiResponseHandler<LoginCommand, LoginResponse>
1012
{
1113
private readonly AppDbContext _context;
1214
private readonly IJwtService _jwtService;
1315
private readonly IExecutionContext _currentUser;
16+
private readonly IPinHasher _pinHasher;
1417

1518
public LoginCommandHandler(
16-
AppDbContext context,
19+
AppDbContext context,
1720
IJwtService jwtService,
18-
IExecutionContext currentUser)
21+
IExecutionContext currentUser,
22+
IPinHasher pinHasher)
1923
{
2024
_context = context;
2125
_jwtService = jwtService;
2226
_currentUser = currentUser;
27+
_pinHasher = pinHasher;
2328
}
2429

25-
public async Task<LoginResponse> Handle(LoginCommand request, CancellationToken cancellationToken)
30+
public async Task<ApiResponse<LoginResponse>> Handle(LoginCommand request, CancellationToken cancellationToken)
2631
{
32+
if (string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
33+
{
34+
return ApiResponse<LoginResponse>.BadRequest("Username and password are required");
35+
}
36+
2737
var user = await _context.Users
28-
.FirstOrDefaultAsync(u => u.UserName == request.UserName, cancellationToken);
38+
.FirstOrDefaultAsync(u =>
39+
(u.UserName == request.UserName || u.Email == request.UserName)
40+
&& u.Active && !u.Deleted,
41+
cancellationToken);
2942

30-
if (user == null || user.Password != request.Password)
43+
if (user == null)
3144
{
32-
throw new UnauthorizedAccessException("Invalid username or password");
45+
return ApiResponse<LoginResponse>.Unauthorized("Invalid username or password");
46+
}
47+
if (!_pinHasher.VerifyPin(request.Password, user.Password))
48+
{
49+
return ApiResponse<LoginResponse>.Unauthorized("Invalid username or password");
3350
}
3451

52+
// Generate JWT token
3553
var token = _jwtService.GenerateToken(user);
3654

37-
return new LoginResponse
55+
// Create response
56+
var loginResponse = new LoginResponse
3857
{
3958
Token = token,
4059
RefreshToken = "",
41-
Expiration = DateTime.Now.AddHours(1)
60+
Expiration = DateTime.Now.AddHours(1),
4261
};
62+
63+
return ApiResponse<LoginResponse>.Success(loginResponse);
4364
}
44-
}
65+
}

Features/Users/Commands/CreateUserCommandHandler.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
using PaymentCoreServiceApi.Core.Entities.UserGenerated;
44
using PaymentCoreServiceApi.Core.Interfaces.Repositories.Read;
55
using PaymentCoreServiceApi.Core.Interfaces.Repositories.Write;
6+
using PaymentCoreServiceApi.Services;
67

78
namespace PaymentCoreServiceApi.Features.Users.Commands;
89
public class CreateUserCommandHandler: IRequestApiResponseHandler<CreateUserCommand, User>
910
{
1011
private readonly IUserWriteRepository _userWriteRepository;
1112
private readonly IUserReadRepository _userReadRepository;
12-
public CreateUserCommandHandler(IUserWriteRepository userWriteRepository, IUserReadRepository userReadRepository)
13+
private readonly IPinHasher _pinHasher;
14+
15+
public CreateUserCommandHandler(IUserWriteRepository userWriteRepository, IUserReadRepository userReadRepository, IPinHasher pinHasher)
1316
{
1417
_userWriteRepository = userWriteRepository;
1518
_userReadRepository = userReadRepository;
19+
_pinHasher = pinHasher;
1620
}
1721

1822
public async Task<ApiResponse<User>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
@@ -32,7 +36,7 @@ public async Task<ApiResponse<User>> Handle(CreateUserCommand request, Cancellat
3236
Age = request.Age,
3337
Email = request.Email,
3438
UserName = request.UserName,
35-
Password = request.Password,
39+
Password = _pinHasher.HashPin(request.Password),
3640
PhoneNumber = request.PhoneNumber,
3741
Address = request.Address,
3842
Active = true

Program.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@
1010
// Add services to the container.
1111
builder.Services.AddControllers();
1212

13+
// Add CORS configuration from appsettings
14+
builder.Services.AddCors(options =>
15+
{
16+
var corsConfig = builder.Configuration.GetSection("Cors");
17+
var allowedOrigins = corsConfig.GetSection("AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
18+
19+
options.AddPolicy("AllowFrontend", policy =>
20+
{
21+
policy.WithOrigins(allowedOrigins)
22+
.AllowAnyMethod()
23+
.AllowAnyHeader()
24+
.AllowCredentials();
25+
});
26+
27+
// Policy cho development - cho phép tất cả
28+
options.AddPolicy("AllowAll", policy =>
29+
{
30+
policy.AllowAnyOrigin()
31+
.AllowAnyMethod()
32+
.AllowAnyHeader();
33+
});
34+
});
35+
1336
builder.Services.AddDbContext<AppDbContext>(options =>
1437
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection")));
1538

@@ -27,11 +50,17 @@
2750

2851
var app = builder.Build();
2952

53+
// Configure CORS - phải đặt trước các middleware khác
3054
if (app.Environment.IsDevelopment())
3155
{
56+
app.UseCors("AllowAll"); // Cho phép tất cả trong development
3257
app.UseSwagger();
3358
app.UseSwaggerUI();
3459
}
60+
else
61+
{
62+
app.UseCors("AllowFrontend"); // Chỉ cho phép các domain cụ thể trong production
63+
}
3564

3665
app.UseHttpsRedirection();
3766

appsettings.Development.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,25 @@
44
"Default": "Information",
55
"Microsoft.AspNetCore": "Warning"
66
}
7+
},
8+
"Cors": {
9+
"AllowedOrigins": [
10+
"http://localhost:3000",
11+
"http://localhost:3001",
12+
"http://localhost:4200",
13+
"http://localhost:5173",
14+
"http://localhost:8080",
15+
"https://localhost:3000",
16+
"https://localhost:3001",
17+
"https://localhost:4200",
18+
"https://localhost:5173",
19+
"https://localhost:8080",
20+
"http://127.0.0.1:3000",
21+
"http://127.0.0.1:5173"
22+
],
23+
"AllowCredentials": true,
24+
"AllowAnyMethod": true,
25+
"AllowAnyHeader": true,
26+
"AllowAllOrigins": true
727
}
828
}

appsettings.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,22 @@
1313
"Issuer": "payment-core-service",
1414
"Audience": "payment-core-clients"
1515
},
16+
"Cors": {
17+
"AllowedOrigins": [
18+
"http://localhost:3000",
19+
"http://localhost:3001",
20+
"http://localhost:4200",
21+
"http://localhost:5173",
22+
"http://localhost:8080",
23+
"https://localhost:3000",
24+
"https://localhost:3001",
25+
"https://localhost:4200",
26+
"https://localhost:5173",
27+
"https://localhost:8080"
28+
],
29+
"AllowCredentials": true,
30+
"AllowAnyMethod": true,
31+
"AllowAnyHeader": true
32+
},
1633
"AllowedHosts": "*"
1734
}

0 commit comments

Comments
 (0)