Middlewares y Seguridad Básica
El módulo 5 es crucial para entender el ciclo de vida de una solicitud HTTP, desde que llega al servidor hasta que genera una respuesta. Abordaremos la Pipeline de Middleware y las capas fundamentales de seguridad.
5.1. La Pipeline de Middleware
El middleware es software que se ensambla en un pipeline para manejar solicitudes y respuestas HTTP. Cada componente de middleware puede examinar, modificar o terminar la solicitud antes de que llegue al endpoint final.
Orden y propósito del middleware
El orden es vital. El middleware que debe ejecutarse primero (ej. manejo de errores y seguridad) debe registrarse antes que el middleware que enruta o ejecuta la lógica del endpoint (ej. UseRouting).
Orden Típico:
- Manejo de Excepciones: (
UseDeveloperExceptionPage/UseExceptionHandler) – Primeros. - Redirección HTTPS: (
UseHttpsRedirection) - CORS: (
UseCors) - Autenticación: (
UseAuthentication) – Antes de la autorización. - Autorización: (
UseAuthorization) – Después de la autenticación. - Enrutamiento: (
UseRoutingyMap*métodos) – Donde la solicitud encuentra su endpoint.
5.2. Manejo de CORS (Cross-Origin Resource Sharing)
CORS es un mecanismo de seguridad del navegador que restringe las solicitudes HTTP realizadas desde un dominio (origen) a un dominio diferente. Es necesario configurarlo si tu frontend reside en un dominio distinto al de tu API.
Configuración de la política de CORS
- Registro de Políticas: Usando
builder.Services.AddCors().builder.Services.AddCors(options => { options.AddPolicy("CorsPolicy", policy => { policy.AllowAnyOrigin() // Opcional: WithOrigins("[http://tu-frontend.com](http://tu-frontend.com)") .AllowAnyHeader() .AllowAnyMethod(); }); }); - Uso del Middleware: Usando
app.UseCors()después deUseRouting.app.UseCors("CorsPolicy");
5.3. Implementación de Autenticación (JWT Placeholder)
La Autenticación es el proceso de verificar la identidad del usuario (¿Quién eres?). La forma más común de autenticar APIs modernas es mediante JSON Web Tokens (JWT).
Configuración de esquemas de autenticación
- Registro: Se usa
builder.Services.AddAuthentication()para configurar el esquema, típicamenteJwtBearer.// Añadir el soporte para Autenticación builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { // ... Configuración del token (Authority, Audience, Keys) }); // Añadir el soporte para Autorización (requerido para usar [Authorize] o RequireAuthorization) builder.Services.AddAuthorization(); - Uso del Middleware: Se requiere
app.UseAuthentication()yapp.UseAuthorization()en la secuencia correcta.
5.4. Implementación de Autorización (Roles y Políticas)
La Autorización es el proceso de determinar qué puede hacer un usuario autenticado (¿Qué puedes hacer?).
Uso de RequireAuthorization()
En Minimal APIs, se restringe el acceso a un endpoint utilizando el método de extensión RequireAuthorization().
// Solo los usuarios autenticados pueden acceder a este endpoint
productosApi.MapPost("/", (TodoItem todo) => { /* ... */ })
.RequireAuthorization();
// Se puede especificar una política o rol
productosApi.MapDelete("/", (int id) => { /* ... */ })
.RequireAuthorization("AdminPolicy");
// Usings necesarios.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
// --------------------------------------------------------------------
// NUEVOS USINGS PARA SEGURIDAD Y MIDDLEWARE (MÓDULO 5)
// --------------------------------------------------------------------
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Security.Claims; // Para simular la información del usuario autenticado
// ====================================================================
// MODELOS Y CONTEXTO DE DATOS (MÓDULO 4 - MANTENIDOS)
// ====================================================================
public class TodoItem
{
public int Id { get; set; }
[Required(ErrorMessage = "El título es obligatorio.")]
[StringLength(100, ErrorMessage = "El título no puede exceder los 100 caracteres.")]
public string Title { get; set; } = string.Empty;
public bool IsComplete { get; set; }
}
public class ApiDbContext : DbContext
{
public ApiDbContext(DbContextOptions<ApiDbContext> options) : base(options) { }
public DbSet<TodoItem> Todos => Set<TodoItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TodoItem>().HasData(
new TodoItem { Id = 1, Title = "Completar Módulo 5 (Seguridad)", IsComplete = false },
new TodoItem { Id = 2, Title = "Revisar Pipeline de Middleware", IsComplete = true }
);
}
}
// ====================================================================
// MODELOS DE CONFIGURACIÓN Y SERVICIOS (MÓDULO 3 - MANTENIDOS)
// ====================================================================
public class SettingsOptions
{
public const string Settings = "Settings";
public string NombreAplicacion { get; set; } = "API por Defecto";
public int MaxItems { get; set; }
}
public interface IItemService
{
List<string> ObtenerDatos();
}
public class MockItemService : IItemService
{
private readonly List<string> _items = new List<string> { "Item A", "Item B", "Item C" };
public List<string> ObtenerDatos() => _items;
}
// ====================================================================
// APLICACIÓN PRINCIPAL (Program.cs)
// ====================================================================
var builder = WebApplication.CreateBuilder(args);
// --- CLAVE SECRETA PARA JWT (DEBERÍA ESTAR EN CONFIGURACIÓN) ---
var jwtKey = builder.Configuration["Jwt:Key"] ?? "EstaEsUnaClaveSuperSecretaDePruebaYDebeTenerMasDe16Caracteres";
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
// --------------------------------------------------------------------
// MÓDULO 5: REGISTRO DE MIDDLEWARE Y SEGURIDAD
// --------------------------------------------------------------------
// 5.2. Configuración de CORS
builder.Services.AddCors(options =>
{
// Define una política de CORS para desarrollo
options.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyOrigin() // Permitir cualquier origen (solo para desarrollo)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// 5.3. Configuración de Autenticación (JWT Bearer)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false, // Desactivado para simplificar la prueba
ValidateAudience = false, // Desactivado para simplificar la prueba
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey
};
});
// 5.4. Configuración de Autorización
builder.Services.AddAuthorization(options =>
{
// Política para el rol "Admin"
options.AddPolicy("AdminPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "Admin"));
});
// --------------------------------------------------------------------
// MÓDULO 4: REGISTRO DE PERSISTENCIA Y SERVICIOS (MANTENIDOS)
// --------------------------------------------------------------------
builder.Services.AddDbContext<ApiDbContext>(options =>
options.UseInMemoryDatabase("MinimalDb"));
builder.Services.AddSingleton<IItemService, MockItemService>();
builder.Services.Configure<SettingsOptions>(
builder.Configuration.GetSection(SettingsOptions.Settings));
builder.Services.AddTransient<OtroServicio>();
var app = builder.Build();
// Inicialización de la base de datos en memoria (opcional pero útil)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApiDbContext>();
db.Database.EnsureCreated();
}
// --------------------------------------------------------------------
// 5.1. PIPELINE DE MIDDLEWARE: ORDEN CRÍTICO
// --------------------------------------------------------------------
// 1. Manejo de Errores (Primero)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// 2. Redirección HTTPS (Opcional)
// app.UseHttpsRedirection();
// 3. Enrutamiento (Antes de Seguridad y Endpoints)
app.UseRouting();
// 4. CORS (Después de UseRouting, Antes de Seguridad)
app.UseCors("CorsPolicy");
// 5. Autenticación (Antes de Autorización)
app.UseAuthentication();
// 6. Autorización (Antes de los Endpoints)
app.UseAuthorization();
// --------------------------------------------------------------------
// ENDPOINTS (MÓDULOS 3 y 4 - MANTENIDOS)
// --------------------------------------------------------------------
// MÓDULO 3 ENDPOINTS
app.MapGet("/api/v1/items-con-servicio", (IItemService itemService) =>
{
var datos = itemService.ObtenerDatos();
return Results.Ok(new { Mensaje = "Datos obtenidos del servicio inyectado", Datos = datos });
})
.WithName("ItemsConServicio");
app.MapGet("/api/v1/configuracion", (IOptions<SettingsOptions> options) =>
{
var settings = options.Value;
return Results.Ok(new
{
settings.NombreAplicacion,
settings.MaxItems,
Mensaje = "Opciones de configuración inyectadas correctamente."
});
})
.WithName("Configuracion");
app.MapDemoGroups(); // Llama al grupo de rutas V2
// MÓDULO 4: ENDPOINTS CRUD CON PERSISTENCIA Y VALIDACIÓN
var todoApi = app.MapGroup("/api/v1/todos")
.WithTags("ToDos (EF Core y Seguridad)");
// Endpoint Abierto (No requiere autenticación)
todoApi.MapGet("/", async (ApiDbContext db) =>
await db.Todos.ToListAsync())
.WithName("GetTodos");
// Endpoint Abierto por ID
todoApi.MapGet("/{id:int}", async (int id, ApiDbContext db) =>
await db.Todos.FindAsync(id)
is TodoItem todo
? Results.Ok(todo)
: Results.NotFound());
// --------------------------------------------------------------------
// 5.4. AUTORIZACIÓN: Endpoints Restringidos
// --------------------------------------------------------------------
// POST: Requiere Autenticación (cualquier usuario)
todoApi.MapPost("/", async (TodoItem todo, ApiDbContext db) =>
{
if (string.IsNullOrWhiteSpace(todo.Title) || todo.Title.Length > 100)
{
return Results.BadRequest(new { Error = "Validación fallida para 'Title'." });
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/api/v1/todos/{todo.Id}", todo);
})
.RequireAuthorization() // <<--- Requiere que el usuario esté autenticado
.WithName("CreateTodo");
// DELETE: Requiere Política "AdminPolicy" (Usuario con Claim Role=Admin)
todoApi.MapDelete("/{id:int}", async (int id, ApiDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
})
.RequireAuthorization("AdminPolicy") // <<--- Requiere el rol "Admin"
.WithName("DeleteTodo");
// --- Endpoint de SIMULACIÓN DE LOGIN para obtener un token de prueba ---
app.MapPost("/api/login", (LoginRequest request) =>
{
// VALIDACIÓN SIMPLE DE USUARIO Y CONTRASEÑA
if (request.Username != "testuser" || request.Password != "password")
{
return Results.Unauthorized();
}
// 1. Definir Claims (Identidad y Roles)
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Email, $"{request.Username}@test.com"),
// Asignar el rol 'Admin' solo para el usuario de prueba
new Claim(ClaimTypes.Role, "Admin")
};
// 2. Crear Token
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
// Issuer y Audience se omiten aquí, pero se configurarían en un ambiente real.
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var tokenString = tokenHandler.WriteToken(token);
return Results.Ok(new { token = tokenString, message = "Token generado. Úsalo como 'Bearer <token>'" });
});
// Modelo de solicitud para el login
public record LoginRequest(string Username, string Password);
// --------------------------------------------------------------------
// EXTENSIÓN PARA AGRUPACIÓN Y MANEJO DE ENDPOINTS FUERA DE Program.cs (MÓDULO 3 - MANTENIDOS)
// --------------------------------------------------------------------
public class OtroServicio
{
public string ObtenerInfo() => "Información de un servicio del grupo de rutas.";
}
public static class RouteGroupExtensions
{
public static void MapDemoGroups(this WebApplication app)
{
var grupoV2 = app.MapGroup("/api/v2/grupo")
.WithTags("Grupo V2");
grupoV2.MapGet("/saludo", () =>
{
return Results.Ok("¡Hola desde el Grupo V2!");
})
.WithName("GrupoV2Saludo");
grupoV2.MapGet("/info", (OtroServicio otroServicio) =>
{
return Results.Ok(new { Mensaje = otroServicio.ObtenerInfo() });
})
.WithName("GrupoV2Info");
}
}