Files
Chatbot-ElDia/ChatbotApi/Program.cs

206 lines
7.1 KiB
C#
Raw Normal View History

2025-11-18 14:34:26 -03:00
using ChatbotApi.Data.Models;
using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.OpenApi.Models;
using Microsoft.EntityFrameworkCore; // Necesario para UseSqlServer
2025-11-18 14:34:26 -03:00
// Cargar variables de entorno
2025-11-18 14:34:26 -03:00
Env.Load();
var builder = WebApplication.CreateBuilder(args);
// [SEGURIDAD] Configuración de Kestrel para ocultar el header "Server" (Information Disclosure)
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
});
// [SEGURIDAD] CORS Restrictivo
2025-11-18 14:34:26 -03:00
var myAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: myAllowSpecificOrigins,
policy =>
{
policy.WithOrigins(
"http://192.168.5.129:8081",
"http://192.168.5.129:8082",
"http://localhost:5173",
"http://localhost:5174",
"http://localhost:5175")
2025-11-18 14:34:26 -03:00
.AllowAnyHeader()
.AllowCredentials() // [SEGURIDAD] Necesario para Cookies HttpOnly
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
2025-11-18 14:34:26 -03:00
});
});
// 1. DbContext
2025-11-18 14:34:26 -03:00
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppContexto>(options =>
options.UseSqlServer(connectionString));
// 2. Identity
2025-11-18 14:34:26 -03:00
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
// [SEGURIDAD] Políticas de contraseñas robustas
2025-11-18 14:34:26 -03:00
options.Password.RequireDigit = true;
options.Password.RequiredLength = 12; // Aumentado de 8 a 12
options.Password.RequireNonAlphanumeric = true; // Requerimos símbolos
2025-11-18 14:34:26 -03:00
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
// [SEGURIDAD] Bloqueo de cuenta tras intentos fallidos (Mitigación Fuerza Bruta)
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
2025-11-18 14:34:26 -03:00
})
.AddEntityFrameworkStores<AppContexto>()
.AddDefaultTokenProviders();
builder.Services.AddMemoryCache();
// JWT Config
2025-11-18 14:34:26 -03:00
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("La clave JWT no está configurada.")
)),
ClockSkew = TimeSpan.Zero // Token expira exactamente cuando dice
2025-11-18 14:34:26 -03:00
};
// [SEGURIDAD] Evento para permitir lectura de Token desde Cookie
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey("X-Access-Token"))
{
context.Token = context.Request.Cookies["X-Access-Token"];
}
return Task.CompletedTask;
}
};
2025-11-18 14:34:26 -03:00
});
// [SEGURIDAD] RATE LIMITING AVANZADO
2025-11-18 14:34:26 -03:00
builder.Services.AddRateLimiter(options =>
{
// Política General: 30 peticiones por minuto (Suficiente para uso normal del chat)
2025-11-18 14:34:26 -03:00
options.AddFixedWindowLimiter(policyName: "fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 30;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 2;
2025-11-18 14:34:26 -03:00
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
// [SEGURIDAD] Política Estricta para Login: 5 intentos por minuto (Anti Fuerza Bruta)
options.AddFixedWindowLimiter(policyName: "login-limit", limiterOptions =>
{
limiterOptions.PermitLimit = 5;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 0; // No encolar, rechazar directo
});
2025-11-18 14:34:26 -03:00
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Chatbot API", Version = "v1" });
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme.",
2025-11-18 14:34:26 -03:00
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer"
2025-11-18 14:34:26 -03:00
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
2025-11-18 14:34:26 -03:00
},
new string[] {}
}
});
});
2025-12-09 10:28:18 -03:00
// [REFACTORING] Servicios de Aplicación
builder.Services.AddHttpClient(); // Cliente HTTP eficiente
builder.Services.AddScoped<ChatbotApi.Services.IChatService, ChatbotApi.Services.ChatService>();
2025-11-18 14:34:26 -03:00
var app = builder.Build();
// [SEGURIDAD] Headers de Seguridad HTTP
// Se recomienda usar la librería 'NetEscapades.AspNetCore.SecurityHeaders'
// Si no la tienes, instálala: dotnet add package NetEscapades.AspNetCore.SecurityHeaders
2025-11-18 14:34:26 -03:00
app.UseSecurityHeaders(policy =>
{
policy.AddDefaultSecurityHeaders();
policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365); // HSTS 1 año
2025-11-18 14:34:26 -03:00
policy.AddContentSecurityPolicy(builder =>
{
builder.AddDefaultSrc().Self();
// Permitimos scripts inline solo si es estrictamente necesario para Swagger, idealmente usar nonces
2025-11-18 14:34:26 -03:00
builder.AddScriptSrc().Self().UnsafeInline();
builder.AddStyleSrc().Self().UnsafeInline();
builder.AddImgSrc().Self().Data();
builder.AddConnectSrc().Self()
.From("http://localhost:5173")
.From("http://localhost:5174")
.From("http://localhost:5175"); // Permitir conexiones explícitas
builder.AddFrameAncestors().None(); // Previene Clickjacking
2025-11-18 14:34:26 -03:00
});
policy.RemoveServerHeader(); // Capa extra para ocultar Kestrel
2025-11-18 14:34:26 -03:00
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
// [SEGURIDAD] Forzar HTTPS en producción
app.UseHsts();
}
app.UseHttpsRedirection();
2025-11-18 14:34:26 -03:00
app.UseCors(myAllowSpecificOrigins);
// [SEGURIDAD] Aplicar Rate Limiting
2025-11-18 14:34:26 -03:00
app.UseRateLimiter();
2025-11-18 14:34:26 -03:00
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Mapeamos los controladores. Las políticas de Rate Limiting se aplicarán vía Atributos en cada Controller.
2025-11-18 14:34:26 -03:00
app.MapControllers();
app.Run();