577 lines
17 KiB
Markdown
577 lines
17 KiB
Markdown
# Security Patterns - Detailed Reference
|
|
|
|
Detailed code examples for security patterns in ASP.NET Core.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Authentication with Identity](#authentication-with-identity)
|
|
2. [JWT Configuration](#jwt-configuration)
|
|
3. [Authorization Policies](#authorization-policies)
|
|
4. [Password Hashing](#password-hashing)
|
|
5. [Two-Factor Authentication](#two-factor-authentication)
|
|
6. [Input Validation](#input-validation)
|
|
7. [Rate Limiting](#rate-limiting)
|
|
8. [Global Exception Handler](#global-exception-handler)
|
|
9. [Audit Logging](#audit-logging)
|
|
10. [CORS Configuration](#cors-configuration)
|
|
|
|
---
|
|
|
|
## Authentication with Identity
|
|
|
|
### Register User Command Handler
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Handler for user registration.
|
|
/// VI: Handler cho việc đăng ký user.
|
|
/// </summary>
|
|
public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, RegisterResult>
|
|
{
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly ILogger<RegisterUserCommandHandler> _logger;
|
|
|
|
public RegisterUserCommandHandler(
|
|
UserManager<ApplicationUser> userManager,
|
|
ILogger<RegisterUserCommandHandler> logger)
|
|
{
|
|
_userManager = userManager;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<RegisterResult> Handle(
|
|
RegisterUserCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// EN: Check if user exists / VI: Kiểm tra user đã tồn tại
|
|
var existingUser = await _userManager.FindByEmailAsync(request.Email);
|
|
if (existingUser != null)
|
|
{
|
|
throw new ConflictException("User with this email already exists");
|
|
}
|
|
|
|
// EN: Create new user / VI: Tạo user mới
|
|
var user = new ApplicationUser
|
|
{
|
|
UserName = request.Email,
|
|
Email = request.Email,
|
|
FullName = request.FullName,
|
|
EmailConfirmed = false
|
|
};
|
|
|
|
// EN: Identity handles password hashing automatically
|
|
// VI: Identity tự động hash password
|
|
var result = await _userManager.CreateAsync(user, request.Password);
|
|
|
|
if (!result.Succeeded)
|
|
{
|
|
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
|
|
throw new ValidationException(errors);
|
|
}
|
|
|
|
// EN: Assign default role / VI: Gán role mặc định
|
|
await _userManager.AddToRoleAsync(user, "User");
|
|
|
|
_logger.LogInformation(
|
|
"EN: User registered successfully / VI: Đăng ký user thành công: {Email}",
|
|
user.Email);
|
|
|
|
return new RegisterResult(user.Id, user.Email!);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## JWT Configuration
|
|
|
|
### Program.cs Configuration
|
|
|
|
```csharp
|
|
// EN: Configure JWT authentication / VI: Cấu hình JWT authentication
|
|
builder.Services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
})
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.Authority = builder.Configuration["IdentityServer:Authority"];
|
|
options.Audience = builder.Configuration["IdentityServer:Audience"];
|
|
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
|
|
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidateAudience = true,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
ClockSkew = TimeSpan.FromMinutes(5)
|
|
};
|
|
|
|
options.Events = new JwtBearerEvents
|
|
{
|
|
OnAuthenticationFailed = context =>
|
|
{
|
|
var logger = context.HttpContext.RequestServices
|
|
.GetRequiredService<ILogger<Program>>();
|
|
logger.LogWarning(
|
|
"EN: JWT authentication failed / VI: JWT authentication thất bại: {Error}",
|
|
context.Exception.Message);
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Authorization Policies
|
|
|
|
### Configure Policies
|
|
|
|
```csharp
|
|
// EN: Configure authorization policies / VI: Cấu hình authorization policies
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
// EN: Role-based policy / VI: Policy dựa trên role
|
|
options.AddPolicy("AdminOnly", policy =>
|
|
policy.RequireRole("Admin"));
|
|
|
|
// EN: Multiple roles / VI: Nhiều roles
|
|
options.AddPolicy("CanManageUsers", policy =>
|
|
policy.RequireRole("Admin", "Manager"));
|
|
|
|
// EN: Claim-based policy / VI: Policy dựa trên claim
|
|
options.AddPolicy("PremiumUser", policy =>
|
|
policy.RequireClaim("subscription", "premium"));
|
|
|
|
// EN: Custom requirement / VI: Requirement tùy chỉnh
|
|
options.AddPolicy("ResourceOwner", policy =>
|
|
policy.AddRequirements(new ResourceOwnerRequirement()));
|
|
});
|
|
|
|
/// <summary>
|
|
/// EN: Custom authorization requirement.
|
|
/// VI: Authorization requirement tùy chỉnh.
|
|
/// </summary>
|
|
public class ResourceOwnerRequirement : IAuthorizationRequirement { }
|
|
|
|
public class ResourceOwnerHandler : AuthorizationHandler<ResourceOwnerRequirement>
|
|
{
|
|
protected override Task HandleRequirementAsync(
|
|
AuthorizationHandlerContext context,
|
|
ResourceOwnerRequirement requirement)
|
|
{
|
|
var userId = context.User.FindFirst("sub")?.Value;
|
|
var resourceOwnerId = context.Resource as string;
|
|
|
|
if (userId == resourceOwnerId)
|
|
{
|
|
context.Succeed(requirement);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Password Hashing
|
|
|
|
### ASP.NET Core Identity (Automatic)
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Identity uses PBKDF2 with HMAC-SHA256 by default.
|
|
/// VI: Identity sử dụng PBKDF2 với HMAC-SHA256 mặc định.
|
|
/// </summary>
|
|
|
|
// EN: Creating user with password (auto-hashed)
|
|
// VI: Tạo user với password (tự động hash)
|
|
await _userManager.CreateAsync(user, password);
|
|
|
|
// EN: Verify password / VI: Xác minh password
|
|
var isValid = await _userManager.CheckPasswordAsync(user, password);
|
|
|
|
// EN: Change password / VI: Đổi password
|
|
await _userManager.ChangePasswordAsync(user, currentPassword, newPassword);
|
|
|
|
// EN: Reset password with token / VI: Reset password với token
|
|
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
|
await _userManager.ResetPasswordAsync(user, token, newPassword);
|
|
```
|
|
|
|
### Custom Password Hasher (If Needed)
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Configure password hasher options.
|
|
/// VI: Cấu hình password hasher options.
|
|
/// </summary>
|
|
builder.Services.Configure<PasswordHasherOptions>(options =>
|
|
{
|
|
// EN: Iteration count (higher = more secure but slower)
|
|
// VI: Số lần lặp (cao hơn = bảo mật hơn nhưng chậm hơn)
|
|
options.IterationCount = 100000;
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Two-Factor Authentication
|
|
|
|
### Enable 2FA Command Handler
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Handler for enabling 2FA.
|
|
/// VI: Handler cho việc bật 2FA.
|
|
/// </summary>
|
|
public class Enable2FACommandHandler : IRequestHandler<Enable2FACommand, Enable2FAResult>
|
|
{
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private const string AuthenticatorUriFormat =
|
|
"otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
|
|
|
public async Task<Enable2FAResult> Handle(
|
|
Enable2FACommand request,
|
|
CancellationToken ct)
|
|
{
|
|
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
|
|
if (user == null)
|
|
throw new NotFoundException("User not found");
|
|
|
|
// EN: Check if 2FA already enabled / VI: Kiểm tra 2FA đã bật chưa
|
|
if (await _userManager.GetTwoFactorEnabledAsync(user))
|
|
throw new InvalidOperationException("2FA is already enabled");
|
|
|
|
// EN: Generate authenticator key / VI: Tạo key authenticator
|
|
await _userManager.ResetAuthenticatorKeyAsync(user);
|
|
var key = await _userManager.GetAuthenticatorKeyAsync(user);
|
|
|
|
// EN: Generate QR code / VI: Tạo QR code
|
|
var uri = string.Format(
|
|
AuthenticatorUriFormat,
|
|
"GoodGo",
|
|
user.Email,
|
|
key);
|
|
var qrCode = GenerateQrCode(uri);
|
|
|
|
// EN: Generate recovery codes / VI: Tạo mã khôi phục
|
|
var recoveryCodes = await _userManager
|
|
.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
|
|
|
return new Enable2FAResult
|
|
{
|
|
QrCodeBase64 = qrCode,
|
|
ManualEntryKey = key!,
|
|
RecoveryCodes = recoveryCodes?.ToArray() ?? Array.Empty<string>()
|
|
};
|
|
}
|
|
|
|
private string GenerateQrCode(string uri)
|
|
{
|
|
using var qrGenerator = new QRCodeGenerator();
|
|
using var qrData = qrGenerator.CreateQrCode(uri, QRCodeGenerator.ECCLevel.Q);
|
|
using var qrCode = new PngByteQRCode(qrData);
|
|
var qrBytes = qrCode.GetGraphic(20);
|
|
return Convert.ToBase64String(qrBytes);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Verify 2FA Code
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Verify TOTP code during login.
|
|
/// VI: Xác minh mã TOTP khi đăng nhập.
|
|
/// </summary>
|
|
public async Task<bool> VerifyTwoFactorCodeAsync(
|
|
ApplicationUser user,
|
|
string code)
|
|
{
|
|
// EN: Verify TOTP code / VI: Xác minh mã TOTP
|
|
var isValid = await _userManager
|
|
.VerifyTwoFactorTokenAsync(
|
|
user,
|
|
_userManager.Options.Tokens.AuthenticatorTokenProvider,
|
|
code);
|
|
|
|
if (!isValid)
|
|
{
|
|
// EN: Try recovery code / VI: Thử mã khôi phục
|
|
var result = await _userManager
|
|
.RedeemTwoFactorRecoveryCodeAsync(user, code);
|
|
return result.Succeeded;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Input Validation
|
|
|
|
### FluentValidation Setup
|
|
|
|
```csharp
|
|
// EN: Register FluentValidation / VI: Đăng ký FluentValidation
|
|
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
|
builder.Services.AddFluentValidationAutoValidation();
|
|
|
|
/// <summary>
|
|
/// EN: Validator for register command.
|
|
/// VI: Validator cho register command.
|
|
/// </summary>
|
|
public class RegisterUserCommandValidator : AbstractValidator<RegisterUserCommand>
|
|
{
|
|
public RegisterUserCommandValidator()
|
|
{
|
|
RuleFor(x => x.Email)
|
|
.NotEmpty().WithMessage("Email is required")
|
|
.EmailAddress().WithMessage("Invalid email format");
|
|
|
|
RuleFor(x => x.Password)
|
|
.NotEmpty().WithMessage("Password is required")
|
|
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
|
|
.Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
|
|
.Matches("[a-z]").WithMessage("Password must contain lowercase letter")
|
|
.Matches("[0-9]").WithMessage("Password must contain digit")
|
|
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
|
|
|
|
RuleFor(x => x.FullName)
|
|
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rate Limiting
|
|
|
|
### ASP.NET Core Rate Limiting
|
|
|
|
```csharp
|
|
// EN: Configure rate limiting / VI: Cấu hình rate limiting
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
// EN: Standard rate limit / VI: Rate limit chuẩn
|
|
options.AddFixedWindowLimiter("standard", opt =>
|
|
{
|
|
opt.PermitLimit = 100;
|
|
opt.Window = TimeSpan.FromMinutes(15);
|
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
opt.QueueLimit = 5;
|
|
});
|
|
|
|
// EN: Strict rate limit for sensitive operations
|
|
// VI: Rate limit nghiêm ngặt cho operations nhạy cảm
|
|
options.AddFixedWindowLimiter("strict", opt =>
|
|
{
|
|
opt.PermitLimit = 10;
|
|
opt.Window = TimeSpan.FromHours(1);
|
|
});
|
|
|
|
// EN: Login rate limit / VI: Rate limit cho login
|
|
options.AddSlidingWindowLimiter("login", opt =>
|
|
{
|
|
opt.PermitLimit = 5;
|
|
opt.Window = TimeSpan.FromMinutes(15);
|
|
opt.SegmentsPerWindow = 3;
|
|
});
|
|
|
|
options.OnRejected = async (context, token) =>
|
|
{
|
|
context.HttpContext.Response.StatusCode = 429;
|
|
await context.HttpContext.Response.WriteAsJsonAsync(
|
|
ApiResponse<object>.Fail("RATE_LIMITED", "Too many requests"),
|
|
token);
|
|
};
|
|
});
|
|
|
|
// EN: Apply rate limiting / VI: Áp dụng rate limiting
|
|
app.UseRateLimiter();
|
|
|
|
// EN: Use on specific endpoints / VI: Dùng cho endpoints cụ thể
|
|
[EnableRateLimiting("login")]
|
|
[HttpPost("login")]
|
|
public async Task<IActionResult> Login(...) { }
|
|
```
|
|
|
|
---
|
|
|
|
## Global Exception Handler
|
|
|
|
### Exception Handler Middleware
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Global exception handler middleware.
|
|
/// VI: Middleware xử lý exception toàn cục.
|
|
/// </summary>
|
|
public class ExceptionHandlerMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
|
|
|
|
public ExceptionHandlerMiddleware(
|
|
RequestDelegate next,
|
|
ILogger<ExceptionHandlerMiddleware> logger)
|
|
{
|
|
_next = next;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
try
|
|
{
|
|
await _next(context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await HandleExceptionAsync(context, ex);
|
|
}
|
|
}
|
|
|
|
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
|
|
{
|
|
// EN: Log full error details / VI: Log chi tiết lỗi đầy đủ
|
|
_logger.LogError(ex,
|
|
"EN: Unhandled exception / VI: Exception không xử lý: {Path}",
|
|
context.Request.Path);
|
|
|
|
var (statusCode, message) = ex switch
|
|
{
|
|
ValidationException ve => (400, ve.Message),
|
|
UnauthorizedAccessException => (401, "Unauthorized"),
|
|
NotFoundException => (404, "Resource not found"),
|
|
ConflictException => (409, "Resource already exists"),
|
|
_ => (500, "An unexpected error occurred")
|
|
};
|
|
|
|
context.Response.StatusCode = statusCode;
|
|
context.Response.ContentType = "application/json";
|
|
|
|
// EN: Don't expose internal errors / VI: Không lộ lỗi internal
|
|
var response = ApiResponse<object>.Fail(
|
|
statusCode == 500 ? "INTERNAL_ERROR" : "ERROR",
|
|
message);
|
|
|
|
await context.Response.WriteAsJsonAsync(response);
|
|
}
|
|
}
|
|
|
|
// EN: Register in Program.cs / VI: Đăng ký trong Program.cs
|
|
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
|
```
|
|
|
|
---
|
|
|
|
## Audit Logging
|
|
|
|
### Audit Service
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Service for audit logging.
|
|
/// VI: Service cho audit logging.
|
|
/// </summary>
|
|
public interface IAuditService
|
|
{
|
|
Task LogSecurityEventAsync(
|
|
string eventType,
|
|
Guid? userId,
|
|
object? details,
|
|
HttpContext? context = null);
|
|
}
|
|
|
|
public class AuditService : IAuditService
|
|
{
|
|
private readonly ApplicationDbContext _db;
|
|
private readonly ILogger<AuditService> _logger;
|
|
|
|
public async Task LogSecurityEventAsync(
|
|
string eventType,
|
|
Guid? userId,
|
|
object? details,
|
|
HttpContext? context = null)
|
|
{
|
|
var sanitizedDetails = SanitizeDetails(details);
|
|
|
|
var auditLog = new AuditLog
|
|
{
|
|
EventType = eventType,
|
|
UserId = userId,
|
|
Details = JsonSerializer.Serialize(sanitizedDetails),
|
|
IpAddress = context?.Connection.RemoteIpAddress?.ToString(),
|
|
UserAgent = context?.Request.Headers.UserAgent.FirstOrDefault(),
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
_db.AuditLogs.Add(auditLog);
|
|
await _db.SaveChangesAsync();
|
|
|
|
_logger.LogInformation(
|
|
"EN: Security event / VI: Sự kiện bảo mật: {EventType} UserId: {UserId}",
|
|
eventType, userId);
|
|
}
|
|
|
|
private object? SanitizeDetails(object? details)
|
|
{
|
|
if (details == null) return null;
|
|
|
|
var json = JsonSerializer.Serialize(details);
|
|
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
|
|
|
|
var sensitiveKeys = new[] { "password", "token", "secret", "key" };
|
|
foreach (var key in sensitiveKeys)
|
|
{
|
|
if (dict?.ContainsKey(key) == true)
|
|
dict[key] = "[REDACTED]";
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## CORS Configuration
|
|
|
|
```csharp
|
|
// EN: Configure CORS / VI: Cấu hình CORS
|
|
var allowedOrigins = builder.Configuration
|
|
.GetSection("Cors:AllowedOrigins")
|
|
.Get<string[]>() ?? Array.Empty<string>();
|
|
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("Default", policy =>
|
|
{
|
|
policy.WithOrigins(allowedOrigins)
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader()
|
|
.AllowCredentials()
|
|
.SetPreflightMaxAge(TimeSpan.FromHours(24));
|
|
});
|
|
});
|
|
|
|
// EN: Apply CORS / VI: Áp dụng CORS
|
|
app.UseCors("Default");
|
|
```
|
|
|
|
---
|
|
|
|
## Resources / Tài Nguyên
|
|
|
|
- [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity)
|
|
- [Duende IdentityServer](https://docs.duendesoftware.com/)
|
|
- [FluentValidation](https://docs.fluentvalidation.net/)
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|