Tipo: Ejercicio de implementación enfocada
Duración: 20 minutos
Nivel: Intermedio-Avanzado
Objetivo: Implementar autenticación JWT, autorización basada en scopes, rate limiting y logging estructurado
Al completar este ejercicio, habrás:
- ✅ Implementado autenticación con tokens JWT
- ✅ Configurado autorización basada en scopes (read, write, admin)
- ✅ Aplicado rate limiting por usuario (10 req/min base, 50 req/min premium)
- ✅ Integrado logging estructurado de eventos de seguridad
- ✅ Probado escenarios de acceso autorizado y denegado
Antes de comenzar, verifica que:
- Completaste los Ejercicios 1 y 2 exitosamente
- Tienes
Exercise2Serverfuncionando - Conoces los conceptos de JWT (JSON Web Tokens)
- Entiendes el modelo de autorización basada en roles/scopes
| Concepto | Definición | Pregunta que responde |
|---|---|---|
| Autenticación | Verificar la identidad del usuario | "¿Quién eres?" |
| Autorización | Verificar permisos del usuario | "¿Qué puedes hacer?" |
Ejemplo:
- Autenticación: El usuario presenta un token JWT válido → "Eres Ana García"
- Autorización: El token contiene scope
read→ "Puedes leer, pero no modificar"
Los scopes definen el nivel de acceso:
| Scope | Permisos | Ejemplo de uso |
|---|---|---|
read |
Solo lectura de recursos y herramientas | Consultores externos |
write |
Lectura + modificación de datos | Empleados internos |
admin |
Lectura + modificación + configuración | Administradores del sistema |
Un JWT tiene 3 partes separadas por .:
Header.Payload.Signature
Estructura del Payload (para este ejercicio):
{
"sub": "user-123", // ID del usuario
"name": "Ana García", // Nombre del usuario
"scopes": ["read", "write"], // Permisos
"tier": "premium", // Nivel de servicio
"exp": 1735689600 // Expiración (timestamp)
}Limita las solicitudes por usuario para prevenir abuso:
| Tier | Límite |
|---|---|
| Base | 10 solicitudes/minuto |
| Premium | 50 solicitudes/minuto |
src/McpWorkshop.Servers/
└── Exercise3Server/
├── Program.cs # Servidor principal con seguridad
├── Exercise3Server.csproj # Archivo de proyecto
├── Security/
│ ├── JwtAuthenticationService.cs # Validación de JWT
│ ├── ScopeAuthorizationService.cs # Autorización por scopes
│ └── RateLimitingService.cs # Control de tasa de solicitudes
├── Middleware/
│ ├── AuthenticationMiddleware.cs # Middleware de autenticación
│ └── RateLimitingMiddleware.cs # Middleware de rate limiting
└── Models/
├── AuthenticatedUser.cs # Usuario autenticado
└── RateLimitInfo.cs # Información de rate limit
cd src/McpWorkshop.Servers
dotnet new web -n Exercise3Server -f net10.0
cd Exercise3Server
# Agregar referencias
dotnet add reference ../../McpWorkshop.Shared/McpWorkshop.Shared.csproj
# Agregar paquete JWT
dotnet add package System.IdentityModel.Tokens.Jwt --version 8.16.0
# Agregar a solución
cd ../../..
dotnet sln add src/McpWorkshop.Servers/Exercise3Server/Exercise3Server.csprojcd src/McpWorkshop.Servers/Exercise3Server
mkdir Security
mkdir Middleware
mkdir Models✅ Checkpoint: Proyecto creado con estructura.
Crea Models/AuthenticatedUser.cs:
namespace Exercise3Server.Models;
public class AuthenticatedUser
{
public string UserId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public List<string> Scopes { get; set; } = new();
public string Tier { get; set; } = "base"; // "base" o "premium"
}Crea Models/RateLimitInfo.cs:
namespace Exercise3Server.Models;
public class RateLimitInfo
{
public int RequestCount { get; set; }
public DateTime WindowStart { get; set; }
public int Limit { get; set; }
}✅ Checkpoint: Dos modelos creados.
Crea Security/JwtAuthenticationService.cs:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Exercise3Server.Models;
using Microsoft.IdentityModel.Tokens;
namespace Exercise3Server.Security;
public class JwtAuthenticationService
{
private const string SecretKey = "MCP-Workshop-2025-Super-Secret-Key-DO-NOT-USE-IN-PRODUCTION";
private readonly SymmetricSecurityKey _signingKey;
private readonly JwtSecurityTokenHandler _tokenHandler;
public JwtAuthenticationService()
{
_signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
_tokenHandler = new JwtSecurityTokenHandler();
}
public AuthenticatedUser? ValidateToken(string token)
{
try
{
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
var principal = _tokenHandler.ValidateToken(token, validationParameters, out _);
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var name = principal.FindFirst(ClaimTypes.Name)?.Value;
var scopesClaim = principal.FindFirst("scopes")?.Value;
var tier = principal.FindFirst("tier")?.Value ?? "base";
if (string.IsNullOrEmpty(userId))
{
return null;
}
var scopes = scopesClaim?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List<string>();
return new AuthenticatedUser
{
UserId = userId,
Name = name ?? "Unknown",
Scopes = scopes,
Tier = tier
};
}
catch
{
return null;
}
}
public string GenerateToken(string userId, string name, List<string> scopes, string tier, int expirationMinutes = 60)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, name),
new Claim("scopes", string.Join(",", scopes)),
new Claim("tier", tier)
};
var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
var expires = DateTime.UtcNow.AddMinutes(expirationMinutes);
var token = new JwtSecurityToken(
claims: claims,
expires: expires,
signingCredentials: credentials
);
return _tokenHandler.WriteToken(token);
}
}Crea Security/ScopeAuthorizationService.cs:
using Exercise3Server.Models;
namespace Exercise3Server.Security;
public class ScopeAuthorizationService
{
public bool HasScope(AuthenticatedUser user, string requiredScope)
{
return user.Scopes.Contains(requiredScope, StringComparer.OrdinalIgnoreCase);
}
public bool HasAnyScope(AuthenticatedUser user, params string[] requiredScopes)
{
return requiredScopes.Any(scope => HasScope(user, scope));
}
public string GetRequiredScopeForMethod(string method)
{
return method switch
{
"initialize" => "", // Público
"resources/list" => "", // Público
"resources/read" => "read", // Requiere read
"tools/list" => "", // Público
"tools/call" => "write", // Requiere write
_ => "admin" // Otros requieren admin
};
}
public bool IsAuthorized(AuthenticatedUser user, string method)
{
var requiredScope = GetRequiredScopeForMethod(method);
if (string.IsNullOrEmpty(requiredScope))
{
return true; // Método público
}
return HasScope(user, requiredScope);
}
}Crea Security/RateLimitingService.cs:
using System.Collections.Concurrent;
using Exercise3Server.Models;
namespace Exercise3Server.Security;
public class RateLimitingService
{
private readonly ConcurrentDictionary<string, RateLimitInfo> _userLimits = new();
public bool IsAllowed(AuthenticatedUser user)
{
var limit = user.Tier == "premium" ? 50 : 10;
var userId = user.UserId;
var now = DateTime.UtcNow;
var limitInfo = _userLimits.GetOrAdd(userId, _ => new RateLimitInfo
{
RequestCount = 0,
WindowStart = now,
Limit = limit
});
// Resetear ventana si pasó 1 minuto
if ((now - limitInfo.WindowStart).TotalMinutes >= 1)
{
limitInfo.RequestCount = 0;
limitInfo.WindowStart = now;
}
// Verificar límite
if (limitInfo.RequestCount >= limit)
{
return false;
}
limitInfo.RequestCount++;
return true;
}
public RateLimitInfo GetLimitInfo(string userId)
{
return _userLimits.GetValueOrDefault(userId) ?? new RateLimitInfo { Limit = 10 };
}
}✅ Checkpoint: Tres servicios de seguridad creados.
Crea Middleware/AuthenticationMiddleware.cs:
using Exercise3Server.Security;
namespace Exercise3Server.Middleware;
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly JwtAuthenticationService _authService;
public AuthenticationMiddleware(RequestDelegate next, JwtAuthenticationService authService)
{
_next = next;
_authService = authService;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.ToString();
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
var user = _authService.ValidateToken(token);
if (user != null)
{
context.Items["User"] = user;
}
}
await _next(context);
}
}Crea Middleware/RateLimitingMiddleware.cs:
using Exercise3Server.Security;
using Exercise3Server.Models;
namespace Exercise3Server.Middleware;
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly RateLimitingService _rateLimitService;
public RateLimitingMiddleware(RequestDelegate next, RateLimitingService rateLimitService)
{
_next = next;
_rateLimitService = rateLimitService;
}
public async Task InvokeAsync(HttpContext context)
{
var user = context.Items["User"] as AuthenticatedUser;
if (user == null)
{
// Sin autenticación, pasar al siguiente middleware
await _next(context);
return;
}
if (!_rateLimitService.IsAllowed(user))
{
context.Response.StatusCode = 429; // Too Many Requests
await context.Response.WriteAsJsonAsync(new
{
jsonrpc = "2.0",
error = new
{
code = -32003,
message = "Rate limit exceeded",
data = new
{
userId = user.UserId,
limit = _rateLimitService.GetLimitInfo(user.UserId).Limit,
tier = user.Tier
}
},
id = (object?)null
});
return;
}
await _next(context);
}
}✅ Checkpoint: Dos middlewares creados.
Reemplaza todo el contenido de Program.cs:
using System.Text.Json;
using Exercise3Server.Models;
using Exercise3Server.Middleware;
using Exercise3Server.Security;
using McpWorkshop.Shared.Logging;
using McpWorkshop.Shared.Mcp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// Servicios
builder.Services.AddSingleton<IStructuredLogger, StructuredLogger>();
builder.Services.AddSingleton<JwtAuthenticationService>();
builder.Services.AddSingleton<ScopeAuthorizationService>();
builder.Services.AddSingleton<RateLimitingService>();
builder.Services.Configure<McpWorkshop.Shared.Configuration.WorkshopSettings>(options =>
{
options.Server.Name = "Exercise3Server";
options.Server.Version = "1.0.0";
options.Server.ProtocolVersion = "2024-11-05";
options.Server.Port = 5003;
});
var app = builder.Build();
// Middlewares de seguridad
app.UseMiddleware<AuthenticationMiddleware>();
app.UseMiddleware<RateLimitingMiddleware>();
// Health check endpoint
app.MapGet("/", (IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings) => Results.Ok(new
{
status = "healthy",
server = settings.Value.Server.Name,
version = settings.Value.Server.Version,
timestamp = DateTime.UtcNow
}));
// Endpoint para generar tokens (solo para testing)
app.MapPost("/auth/token", (
[FromBody] TokenRequest request,
JwtAuthenticationService authService) =>
{
var token = authService.GenerateToken(
request.UserId,
request.Name,
request.Scopes,
request.Tier,
60
);
return Results.Ok(new { token });
});
// Endpoint MCP
app.MapPost("/mcp", async (
HttpContext httpContext,
JsonRpcRequest request,
IStructuredLogger logger,
ScopeAuthorizationService authz,
IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings) =>
{
var requestId = request.Id?.ToString() ?? "unknown";
var user = httpContext.Items["User"] as AuthenticatedUser;
logger.LogRequest(request.Method, requestId, new Dictionary<string, object>
{
["method"] = request.Method,
["userId"] = user?.UserId ?? "anonymous",
["scopes"] = user?.Scopes ?? new List<string>()
});
try
{
// Verificar autorización para métodos que requieren autenticación
var requiredScope = authz.GetRequiredScopeForMethod(request.Method);
// Si el método requiere scope y no hay usuario autenticado, denegar acceso
if (!string.IsNullOrEmpty(requiredScope) && user == null)
{
logger.LogError(request.Method, requestId, new Exception("Authentication required"));
return Results.Ok(CreateErrorResponse(-32004, "Authentication required", new
{
requiredScope = requiredScope,
message = "Este método requiere autenticación"
}, request.Id));
}
// Si hay usuario autenticado, verificar autorización
if (user != null && !authz.IsAuthorized(user, request.Method))
{
logger.LogError(request.Method, requestId, new Exception("Unauthorized"));
return Results.Ok(CreateErrorResponse(-32004, "Insufficient permissions", new
{
requiredScope = requiredScope,
userScopes = user.Scopes,
message = $"Requiere scope '{requiredScope}' pero solo tienes: {string.Join(", ", user.Scopes)}"
}, request.Id));
}
var response = request.Method switch
{
"initialize" => HandleInitialize(request.Id, settings),
"resources/list" => HandleResourcesList(request.Id),
"resources/read" => HandleResourcesRead(request.Id, user, authz),
"tools/list" => HandleToolsList(request.Id),
"tools/call" => HandleToolsCall(request.Id, user, authz),
_ => CreateErrorResponse(-32601, "Method not found", null, request.Id)
};
logger.LogResponse(request.Method, requestId, 200, 0);
return Results.Ok(response);
}
catch (Exception ex)
{
logger.LogError(request.Method, requestId, ex);
return Results.Ok(CreateErrorResponse(-32603, "Internal error", ex.Message, request.Id));
}
});
app.Run("http://localhost:5003");
// Handlers
static JsonRpcResponse HandleInitialize(object? requestId, IOptions<McpWorkshop.Shared.Configuration.WorkshopSettings> settings)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
protocolVersion = "2024-11-05",
capabilities = new { resources = new { }, tools = new { } },
serverInfo = new
{
name = settings.Value.Server.Name,
version = settings.Value.Server.Version
}
},
Id = requestId
};
}
static JsonRpcResponse HandleResourcesList(object? requestId)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
resources = new[]
{
new { uri = "mcp://secure-data", name = "Secure Data", description = "Datos sensibles del sistema (lectura requiere scope 'read')", mimeType = "application/json" }
}
},
Id = requestId
};
}
static JsonRpcResponse HandleResourcesRead(object? requestId, AuthenticatedUser? user, ScopeAuthorizationService authz)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
contents = new[]
{
new
{
uri = "mcp://secure-data",
mimeType = "application/json",
text = JsonSerializer.Serialize(new { message = "Datos sensibles", user = user?.Name ?? "Unknown" }, new JsonSerializerOptions { WriteIndented = true })
}
}
},
Id = requestId
};
}
static JsonRpcResponse HandleToolsList(object? requestId)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
tools = new[]
{
new
{
name = "secure_action",
description = "Ejecuta una acción protegida en el sistema (ejecución requiere scope 'write')",
inputSchema = new
{
type = "object",
properties = new { action = new { type = "string" } },
required = new[] { "action" }
}
}
}
},
Id = requestId
};
}
static JsonRpcResponse HandleToolsCall(object? requestId, AuthenticatedUser? user, ScopeAuthorizationService authz)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Result = new
{
content = new[]
{
new
{
type = "text",
text = $"Acción ejecutada por {user?.Name ?? "Unknown"} (scopes: {string.Join(", ", user?.Scopes ?? new List<string>())})"
}
}
},
Id = requestId
};
}
static JsonRpcResponse CreateErrorResponse(int code, string message, object? data, object? id)
{
return new JsonRpcResponse
{
JsonRpc = "2.0",
Error = new JsonRpcError { Code = code, Message = message, Data = data },
Id = id
};
}
record TokenRequest(string UserId, string Name, List<string> Scopes, string Tier);✅ Checkpoint: Compilación sin errores (dotnet build).
cd src/McpWorkshop.Servers/Exercise3Server
dotnet runDeberías ver:
info: Now listening on: http://localhost:5003
Invoke-WebRequest -Uri "http://localhost:5003" -Method GETRespuesta esperada: Status 200 con JSON {"status": "healthy", "server": "Exercise3Server", ...}
Terminal 2:
$body = @{
userId = "user-001"
name = "Ana García"
scopes = @("read")
tier = "base"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5003/auth/token" -Method POST -Body $body -ContentType "application/json"
$tokenRead = $response.token
Write-Host "Token (read): $tokenRead"Resultado esperado: Token JWT devuelto.
✅ PASS
$body = @{
userId = "user-002"
name = "Carlos Pérez"
scopes = @("read", "write")
tier = "premium"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5003/auth/token" -Method POST -Body $body -ContentType "application/json"
$tokenWrite = $response.token
Write-Host "Token (read+write): $tokenWrite"✅ PASS
$body = @{
jsonrpc = "2.0"
method = "resources/read"
params = @{ uri = "mcp://secure-data" }
id = "read-001"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -ContentType "application/json"
} catch {
Write-Host "❌ Error esperado: Unauthorized" -ForegroundColor Yellow
}Resultado esperado: Error de autorización.
✅ PASS
$body = @{
jsonrpc = "2.0"
method = "resources/read"
params = @{ uri = "mcp://secure-data" }
id = "read-002"
} | ConvertTo-Json
$headers = @{ Authorization = "Bearer $tokenRead" }
Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -Headers $headers -ContentType "application/json"Resultado esperado: Datos sensibles devueltos con nombre del usuario.
✅ PASS
$body = @{
jsonrpc = "2.0"
method = "tools/call"
params = @{
name = "secure_action"
arguments = @{ action = "test" }
}
id = "call-001"
} | ConvertTo-Json
$headers = @{ Authorization = "Bearer $tokenRead" }
try {
Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -Headers $headers -ContentType "application/json"
} catch {
Write-Host "❌ Error esperado: Insufficient permissions (requiere 'write')" -ForegroundColor Yellow
}Resultado esperado: Error de permisos insuficientes.
✅ PASS
$body = @{
jsonrpc = "2.0"
method = "tools/call"
params = @{
name = "secure_action"
arguments = @{ action = "test" }
}
id = "call-002"
} | ConvertTo-Json
$headers = @{ Authorization = "Bearer $tokenWrite" }
Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -Headers $headers -ContentType "application/json"Resultado esperado: "Acción ejecutada por Carlos Pérez (scopes: read, write)".
✅ PASS
$headers = @{ Authorization = "Bearer $tokenRead" }
# Enviar 12 solicitudes rápidamente
1..12 | ForEach-Object {
$body = @{
jsonrpc = "2.0"
method = "initialize"
params = @{}
id = "init-$_"
} | ConvertTo-Json
$result = Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -Headers $headers -ContentType "application/json" -ErrorAction SilentlyContinue
if ($result.error) {
Write-Host "Request $_: ❌ Rate limit exceeded" -ForegroundColor Red
} else {
Write-Host "Request $_: ✅ OK" -ForegroundColor Green
}
}Resultado esperado: Primeras 10 solicitudes pasan, las 11 y 12 fallan con error 429 Too Many Requests.
✅ PASS
IMPORTANTE: Los métodos initialize, resources/list y tools/list deben ser públicos (no requieren token).
# Prueba initialize sin token
$body = @{
jsonrpc = "2.0"
method = "initialize"
params = @{}
id = 1
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -ContentType "application/json"
Write-Host "✅ initialize es público: $($response.result.protocolVersion)" -ForegroundColor Green
# Prueba resources/list sin token
$body = @{
jsonrpc = "2.0"
method = "resources/list"
params = @{}
id = 2
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -ContentType "application/json"
Write-Host "✅ resources/list es público: $($response.result.resources.Count) recursos" -ForegroundColor Green
# Prueba tools/list sin token
$body = @{
jsonrpc = "2.0"
method = "tools/list"
params = @{}
id = 3
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5003/mcp" -Method POST -Body $body -ContentType "application/json"
Write-Host "✅ tools/list es público: $($response.result.tools.Count) herramientas" -ForegroundColor GreenResultado esperado: Las tres llamadas funcionan sin necesidad de token Authorization.
✅ PASS
Has completado el ejercicio exitosamente si:
- El servidor compila sin errores
- Puedes generar tokens JWT con diferentes scopes
-
resources/readrequiere scoperead -
tools/callrequiere scopewrite - Los usuarios sin token no pueden acceder a recursos protegidos
- Los usuarios con scope insuficiente reciben error de permisos
- El rate limiting funciona (10 req/min para
base, 50 req/min parapremium) - Los logs estructurados registran eventos de seguridad
Causa: El token no se está enviando correctamente en el header.
Solución: Verifica el formato del header:
$headers = @{ Authorization = "Bearer $token" } # Debe incluir "Bearer "Causa: El método GetRequiredScopeForMethod puede estar devolviendo el scope incorrecto.
Solución: Revisa la lógica en ScopeAuthorizationService.cs:
"resources/read" => "read", // resources/read requiere "read"
"tools/call" => "write", // tools/call requiere "write"Causa: La ventana de tiempo no se está reseteando correctamente.
Solución: Verifica la lógica en RateLimitingService.cs:
if ((now - limitInfo.WindowStart).TotalMinutes >= 1)
{
limitInfo.RequestCount = 0;
limitInfo.WindowStart = now;
}Causa: Reloj del sistema desincronizado.
Solución: Aumenta el tiempo de expiración en GenerateToken:
var token = authService.GenerateToken(userId, name, scopes, tier, 120); // 120 minutosImplementa tokens de refresco para renovar tokens expirados sin volver a autenticar.
Registra todos los eventos de seguridad en una base de datos o archivo:
logger.LogSecurityEvent(new
{
userId = user.UserId,
action = "resources/read",
authorized = true,
timestamp = DateTime.UtcNow
});Combina rate limiting por usuario y por IP para prevenir ataques DDoS.
Implementa jerarquía de scopes donde admin incluye automáticamente write y read.
- Estructura: Header.Payload.Signature
- Claims estándar:
sub,exp,iat - Claims personalizados:
scopes,tier
- Separación de permisos (read, write, admin)
- Validación por método MCP
- Mensajes de error informativos
- Ventanas de tiempo (1 minuto)
- Límites por tier de usuario
- Reseteo automático de ventanas
- Pipeline de procesamiento de solicitudes
- Orden de ejecución (autenticación → rate limiting → endpoint)
- Inyección de dependencias en middlewares
Bloque 7: Seguridad y Gobernanza Sesión (15 min)
En el siguiente bloque el instructor profundizará en:
- Mejores prácticas de seguridad para producción
- Gestión de secretos y certificados
- Auditoría y compliance
- Estrategias de despliegue seguro
- JWT.io: https://jwt.io/ (decodificador de tokens)
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- Rate Limiting Patterns: https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern
Preparado por: Instructor del taller MCP
Versión: 1.0.0
Última actualización: Febrero 2026