using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using System.Reflection; using System.Text; using System.Text.Json; using MyNewProjectName.Infrastructure.Options; using MyNewProjectName.Contracts.Common; namespace MyNewProjectName.WebAPI.Extensions; /// /// Extension methods for configuring services in the dependency injection container /// public static class ServiceCollectionExtensions { /// /// Configures Swagger/OpenAPI documentation with JWT Bearer authentication support /// public static IServiceCollection AddCustomSwagger(this IServiceCollection services) { services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyNewProjectName API", Version = "v1" }); var securityScheme = new OpenApiSecurityScheme { Name = "Authorization", Description = "Enter JWT Bearer token", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT" }; c.AddSecurityDefinition("Bearer", securityScheme); var securityRequirement = new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }; c.AddSecurityRequirement(securityRequirement); // Set the comments path for the Swagger JSON and UI (only if XML file exists) var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { c.IncludeXmlComments(xmlPath); } }); return services; } /// /// Configures JWT Bearer authentication with custom token validation and error handling /// public static IServiceCollection AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration) { var jwtOptionsSection = configuration.GetSection(JwtOptions.SectionName); var jwtOptions = jwtOptionsSection.Get() ?? throw new InvalidOperationException("JwtOptions configuration is missing"); services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ValidateLifetime = true, ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)), ClockSkew = TimeSpan.Zero }; // Log token & claims for debugging options.Events = new JwtBearerEvents { OnMessageReceived = context => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("JwtBearer"); // Có thể bật lại nếu cần debug Authorization header // logger.LogInformation("JWT received. Authorization header: {Header}", context.Request.Headers["Authorization"].ToString()); return Task.CompletedTask; }, OnTokenValidated = context => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("JwtBearer"); var claims = context.Principal?.Claims .Select(c => $"{c.Type}={c.Value}") .ToList() ?? new List(); // logger.LogInformation("JWT validated. Claims: {Claims}", string.Join(", ", claims)); return Task.CompletedTask; }, OnAuthenticationFailed = context => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("JwtBearer"); logger.LogWarning(context.Exception, "JWT authentication failed"); return Task.CompletedTask; }, OnChallenge = async context => { // Ngăn không cho middleware mặc định ghi response lần nữa context.HandleResponse(); var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("JwtBearer"); if (context.Response.HasStarted) { logger.LogWarning("The response has already started, the JWT challenge middleware will not be executed."); return; } var correlationId = context.HttpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? "unknown"; var isExpired = context.AuthenticateFailure is SecurityTokenExpiredException; var message = isExpired ? "Token expired" : "Unauthorized"; logger.LogInformation("JWT challenge. Expired: {Expired}, Error: {Error}, Description: {Description}, CorrelationId: {CorrelationId}", isExpired, context.Error, context.ErrorDescription, correlationId); context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json"; var response = new ServiceResponse { Success = false, Message = message, Errors = null }; var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); await context.Response.WriteAsync(json); }, OnForbidden = async context => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("JwtBearer"); logger.LogWarning("User is authenticated but does not have access to the requested resource (403 Forbidden)."); context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.ContentType = "application/json"; var response = new ServiceResponse { Success = false, Message = "You do not have permission to perform this action.", Errors = null }; var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); await context.Response.WriteAsync(json); } }; }); return services; } // /// // /// Configures authorization policies based on user type and roles. // /// // public static IServiceCollection AddCustomAuthorization(this IServiceCollection services) // { // services.AddAuthorization(options => // { // // 1. Policy chỉ dành cho SuperAdmin // options.AddPolicy("RequireSuperAdmin", policy => // policy.RequireClaim("userType", "SuperAdmin")); // // 2. Policy cho TenantAdmin (TenantMember + Role = TenantAdmin) // options.AddPolicy("RequireTenantAdmin", policy => // policy.RequireClaim("userType", "TenantMember") // .RequireRole("TenantAdmin")); // // 3. Policy cho TenantMember (TenantMember + Role = Doctor) // options.AddPolicy("RequireTenantMember", policy => // policy.RequireClaim("userType", "TenantMember") // ); // // 4. Policy SuperAdmin hoặc TenantAdmin // options.AddPolicy("RequireSuperAdminOrTenantAdmin", policy => // policy.RequireAssertion(context => // // Điều kiện 1: Là SuperAdmin // context.User.HasClaim("userType", "SuperAdmin") // || // // HOẶC Điều kiện 2: Là Nhân viên VÀ có Role là TenantAdmin // (context.User.HasClaim("userType", "TenantMember") && context.User.IsInRole("TenantAdmin")) // )); // }); // return services; // } }