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;
|
2025-11-27 15:11:54 -03:00
|
|
|
using Microsoft.EntityFrameworkCore; // Necesario para UseSqlServer
|
2025-11-18 14:34:26 -03:00
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// Cargar variables de entorno
|
2025-11-18 14:34:26 -03:00
|
|
|
Env.Load();
|
|
|
|
|
|
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// [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 =>
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
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()
|
2025-11-27 15:11:54 -03:00
|
|
|
// [SEGURIDAD] Solo permitimos los verbos necesarios. Bloqueamos TRACE, HEAD, etc.
|
|
|
|
|
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
|
2025-11-18 14:34:26 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-27 15:11:54 -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));
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// 2. Identity
|
2025-11-18 14:34:26 -03:00
|
|
|
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
// [SEGURIDAD] Políticas de contraseñas robustas
|
2025-11-18 14:34:26 -03:00
|
|
|
options.Password.RequireDigit = true;
|
2025-11-27 15:11:54 -03:00
|
|
|
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;
|
2025-11-27 15:11:54 -03:00
|
|
|
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();
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// 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.")
|
2025-11-27 15:11:54 -03:00
|
|
|
)),
|
|
|
|
|
ClockSkew = TimeSpan.Zero // Token expira exactamente cuando dice
|
2025-11-18 14:34:26 -03:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// [SEGURIDAD] RATE LIMITING AVANZADO
|
2025-11-18 14:34:26 -03:00
|
|
|
builder.Services.AddRateLimiter(options =>
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
// 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 =>
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
limiterOptions.PermitLimit = 30;
|
|
|
|
|
limiterOptions.Window = TimeSpan.FromMinutes(1);
|
|
|
|
|
limiterOptions.QueueLimit = 2;
|
2025-11-18 14:34:26 -03:00
|
|
|
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// [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,
|
2025-11-27 15:11:54 -03:00
|
|
|
Description = "JWT Authorization header using the Bearer scheme.",
|
2025-11-18 14:34:26 -03:00
|
|
|
Name = "Authorization",
|
2025-11-27 15:11:54 -03:00
|
|
|
Type = SecuritySchemeType.Http,
|
|
|
|
|
Scheme = "bearer"
|
2025-11-18 14:34:26 -03:00
|
|
|
});
|
|
|
|
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
|
|
|
{
|
|
|
|
|
{
|
|
|
|
|
new OpenApiSecurityScheme
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
|
2025-11-18 14:34:26 -03:00
|
|
|
},
|
|
|
|
|
new string[] {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var app = builder.Build();
|
|
|
|
|
|
2025-11-27 15:11:54 -03:00
|
|
|
// [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 =>
|
|
|
|
|
{
|
2025-11-27 15:11:54 -03:00
|
|
|
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();
|
2025-11-27 15:11:54 -03:00
|
|
|
// 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();
|
2025-11-27 15:11:54 -03:00
|
|
|
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
|
|
|
});
|
2025-11-27 15:11:54 -03:00
|
|
|
policy.RemoveServerHeader(); // Capa extra para ocultar Kestrel
|
2025-11-18 14:34:26 -03:00
|
|
|
});
|
2025-11-27 15:11:54 -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);
|
2025-11-27 15:11:54 -03:00
|
|
|
|
|
|
|
|
// [SEGURIDAD] Aplicar Rate Limiting
|
2025-11-18 14:34:26 -03:00
|
|
|
app.UseRateLimiter();
|
2025-11-27 15:11:54 -03:00
|
|
|
|
2025-11-18 14:34:26 -03:00
|
|
|
app.UseAuthentication();
|
|
|
|
|
app.UseAuthorization();
|
2025-11-27 15:11:54 -03:00
|
|
|
|
|
|
|
|
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();
|
2025-11-27 15:11:54 -03:00
|
|
|
|
|
|
|
|
app.Run();
|