Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

namespace FileService.Api.Controllers
{
// [Authorize]
// [Authorize(Policy = "Permission")]
// [Authorize(Roles = "ADMIN")]
// [Authorize]
// [Authorize(Policy = "AdminOnly")]
// [Authorize(Roles = "ADMIN")]
// [AllowAnonymous]
[Route("file/api/[controller]")]
[ApiController]
public class FileDocumentController : BaseApiController
Expand Down
26 changes: 25 additions & 1 deletion FileService/FileService.Api/Middlewares/ExceptionMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using FileService.Core.ApiModels;
using FileService.Core.Enums;
using FileService.Core.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Data;
using System.Net;
using System.Security;
using System.Text;

namespace FileService.Api.Middlewares
Expand Down Expand Up @@ -31,14 +33,19 @@ public async Task InvokeAsync(HttpContext httpContext)
// await LogResuest(httpContext);
await _next(httpContext);


// log the outgoing response
await LogResponse(httpContext);
}
catch (ErrorException ex)
{
await HandleCustomExceptionAsync(httpContext, ex);
}
catch(UnauthorizedAccessException ex)
catch (SecurityException ex) // Thêm cho 403
{
await HandleForbiddenExceptionAsync(httpContext, ex);
}
catch (UnauthorizedAccessException ex)
{
await HandleUnauthorizedExceptionAsync(httpContext, ex);
}
Expand All @@ -55,6 +62,23 @@ public async Task InvokeAsync(HttpContext httpContext)
}
}

private async Task HandleForbiddenExceptionAsync(HttpContext context, SecurityException exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
var apiResponse = new ApiResponseModel(StatusCodeEnum.Forbidden)
{
Message = "Access denied due to insufficient permissions.",
Result = null
};
var jsonResponse = JsonConvert.SerializeObject(apiResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
await context.Response.WriteAsync(jsonResponse);
_logger.LogWarning("Forbidden access - {Message}", exception.Message);
}


private async Task HandleCustomExceptionAsync(HttpContext context, ErrorException exception)
{
Expand Down
117 changes: 117 additions & 0 deletions FileService/FileService.Api/Middlewares/RoleCheckMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using FileService.Core.ApiModels;
using FileService.Core.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace FileService.Api.Middlewares
{
public class RoleCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RoleCheckMiddleware> _logger;
private readonly IMemoryCache _cache;

public RoleCheckMiddleware(RequestDelegate next, ILogger<RoleCheckMiddleware> logger, IMemoryCache cache)
{
_next = next;
_logger = logger;
_cache = cache;
}

public async Task InvokeAsync(HttpContext context)
{
// Bỏ qua middleware nếu endpoint có [AllowAnonymous]
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
_logger.LogDebug("Bypassing RoleCheckMiddleware for anonymous endpoint: {Path}", context.Request.Path);
await _next(context);
return;
}

// Lấy token từ header
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrEmpty(token))
{
_logger.LogWarning("Missing token in request to {Path}", context.Request.Path);
await WriteError(context, StatusCodeEnum.Unauthorized, "Missing token");
return;
}

try
{
// Kiểm tra cache trước khi decode token
var cacheKey = $"claims_{token}";
List<Claim> claims;
if (!_cache.TryGetValue(cacheKey, out claims))
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(token);

var roles = jwtToken.Claims
.Where(c => c.Type == "role" || c.Type == "roles")
.Select(c => c.Value)
.ToList();
_logger.LogInformation("Roles extracted from token: {Roles}", string.Join(", ", roles));

if (!roles.Any())
{
await WriteError(context, StatusCodeEnum.Forbidden, "No roles found in token");
return;
}
claims = jwtToken.Claims.ToList();
_cache.Set(cacheKey, claims, TimeSpan.FromMinutes(5));
}
else
{
// Check roles again from cached claims
var roles = claims.Where(c => c.Type == "role" || c.Type == "roles").Select(c => c.Value).ToList();
_logger.LogInformation("Roles extracted from cached claims: {Roles}", string.Join(", ", roles));
if (!roles.Any())
{
_logger.LogWarning("No roles found in cached token claims for request to {Path}", context.Request.Path);
await WriteError(context, StatusCodeEnum.Forbidden, "No roles found in cached token claims");
return;
}
}

// Gắn claims vào context.User
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
_logger.LogInformation("Passing RoleCheckMiddleware successfully for {Path}", context.Request.Path);

await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing token for request to {Path}", context.Request.Path);
await WriteError(context, StatusCodeEnum.Unauthorized, "Invalid token format");
}
}

private async Task WriteError(HttpContext context, StatusCodeEnum status, string message)
{
context.Response.StatusCode = (int)status;
context.Response.ContentType = "application/json";

var response = new ApiResponseModel<object>(status)
{
Result = null,
Message = message
};

await context.Response.WriteAsJsonAsync(response);
}
}

public static class RoleCheckMiddlewareExtensions
{
public static IApplicationBuilder UseRoleCheck(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RoleCheckMiddleware>();
}
}
}
53 changes: 45 additions & 8 deletions FileService/FileService.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using FileService.Service.Interfaces;
using Microsoft.Extensions.FileProviders;
using System.Security.Claims;
using Microsoft.AspNetCore.DataProtection;

BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));

Expand All @@ -40,7 +41,6 @@

var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
Console.WriteLine($"ASPNETCORE_ENVIRONMENT: {env}");
//builder.Services.Configure<MinioConfig>(builder.Configuration.GetSection("MinioConfig"));

builder.Services.Configure<MongoDbSettings>(builder.Configuration.GetSection("MongoDbSettings"));
builder.Services.Configure<FfmpegSettings>(builder.Configuration.GetSection("FfmpegSettings"));
Expand Down Expand Up @@ -84,6 +84,14 @@
options.Password.RequiredUniqueChars = 1;
});

// Add Data Protection with persistence and encryption
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/root/.aspnet/DataProtection-Keys"))
.ProtectKeysWithDpapi() // Use DPAPI for Windows compatibility
.SetApplicationName("FileService");


//add authen
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Expand All @@ -93,18 +101,34 @@
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidIssuer = appSettings.Jwt.Issuer,
ValidAudience = appSettings.Jwt.Audience,
// ValidAudience = appSettings.Jwt.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.Jwt.Key)),

// Cấu hình claim để nhận Role
RoleClaimType = ClaimTypes.Role

// RoleClaimType = "roles"
};
options.TokenValidationParameters.RoleClaimType = "roles"; // Khớp với token
});


// Thêm MemoryCache để cache claims
builder.Services.AddMemoryCache();

//add author policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("ADMIN"));
options.AddPolicy("TeacherOrAdmin", policy => policy.RequireRole("TEACHER", "SYS_ADMIN"));
options.AddPolicy("SysAdminOnly", policy => policy.RequireRole("SYS_ADMIN"));
options.AddPolicy("OrgAdminOnly", policy => policy.RequireRole("ORG_ADMIN"));
options.AddPolicy("TeacherOnly", policy => policy.RequireRole("TEACHER"));
options.AddPolicy("UserOnly", policy => policy.RequireRole("USER"));
options.AddPolicy("LoggedInUsers", policy => policy.RequireRole("USER", "TEACHER", "ORG_ADMIN", "SYS_ADMIN"));
});

builder.Services.AddCors(options =>
Expand Down Expand Up @@ -164,6 +188,15 @@
options.Limits.MaxRequestBodySize = 6L * 1024 * 1024 * 1024; // 6GB
});

// Configure HTTP client for MinIO with SSL handling
builder.Services.AddHttpClient("MinioClient").ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true // Temporary for dev/staging, remove in production
};
});


var app = builder.Build();

Expand All @@ -183,10 +216,14 @@

app.UseHttpsRedirection();
app.UseAuthentication();
// Middleware pipeline
//app.UseRoleCheck();
app.UseAuthorization();

app.UseMiddleware<ExceptionMiddleware>();

app.UseMiddleware<AuthenMiddleware>();
app.UseMiddleware<ExceptionMiddleware>();
//app.UseMiddleware<RoleCheckMiddleware>();
app.UseMiddleware<UserContextMiddleware>();

app.MapControllers();
Expand Down
2 changes: 1 addition & 1 deletion FileService/FileService.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"Jwt": {
"Key": "1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuG5SIf/1y/LgJxHnMqaF6A/ij",
"Issuer": "Code Campus",
"Audience": "Code Campus",
//"Audience": "Code Campus",
"AccessTokenExpiresTime": 60,
"RefreshTokenExpiresTime": 300
},
Expand Down
12 changes: 11 additions & 1 deletion FileService/FileService.Api/appsettings.Staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"Jwt": {
"Key": "1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuGSSIf/1y/LgJxHnMqaF6A/ij",
"Issuer": "Code Campus",
"Audience": "Code Campus",
// "Audience": "Code Campus",
"AccessTokenExpiresTime": 60,
"RefreshTokenExpiresTime": 3
},
Expand All @@ -43,5 +43,15 @@
"BucketName": "",
"Secure": false
}
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://+:8082"
},
"Https": {
"Url": "https://+:443"
}
}
}
}
2 changes: 1 addition & 1 deletion FileService/FileService.Core/ApiModels/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class Jwt
{
public string Key { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
// public string Audience { get; set; }
public int AccessTokenExpiresTime { get; set; }
public int RefreshTokenExpiresTime { get; set; }
}
Expand Down
22 changes: 18 additions & 4 deletions FileService/FileService.Core/Enums/StatusCodeEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ namespace FileService.Core.Enums
{
public enum StatusCodeEnum
{
[Description("Thành công.")]
Success = 2008200,

[Description("Lỗi hệ thống.")]
Error = 5008201,
Expand Down Expand Up @@ -51,13 +49,29 @@ public enum StatusCodeEnum
[Description("Download Interrupted. Please check your internet connection and try again.")]
C05 = 5038214,

[Description("Bad request.")]
BadRequest = 4008215,

[Description("Invalid filter option.")]
InvalidOption = 4008216,

[Description("Unmatched columns found.")]
UnmatchedColumns = 4008217,

[Description("Success")]
Success = 200,

[Description("Bad Request")]
BadRequest = 400,

[Description("Unauthorized")]
Unauthorized = 401,

[Description("Forbidden")]
Forbidden = 403,

[Description("Not Found")]
NotFound = 404,

[Description("Internal Server Error")]
InternalServerError = 500
}
}
4 changes: 4 additions & 0 deletions docker/docker-compose.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ services:
environment:
# cấu hình môi trường ASP.NET
- ASPNETCORE_ENVIRONMENT=staging
- ASPNETCORE_URLS=http://+:8082;https://+:443
- ASPNETCORE_HTTPS_PORT=443
# ---- MongoDB (file-db) ----
- MongoDbSettings__ConnectionStrings="mongodb://${FILE_USERNAME}:${FILE_PASSWORD}@file-db:27017/${FILE_DATABASE}?authSource=${FILE_DATABASE}"
- MongoDbSettings__DatabaseName=${FILE_DATABASE}
Expand All @@ -165,6 +167,8 @@ services:
- AppSettings__MinioConfig__Secure=false
ports:
- "8082:8082"
volumes:
- C:\DockerVolumes\DataProtectionKeys:/root/.aspnet/DataProtection-Keys
networks: [ backend ]

gateway-service:
Expand Down