diff --git a/src/api/SIGCM2.Api/Authorization/ForbiddenProblemDetailsHandler.cs b/src/api/SIGCM2.Api/Authorization/ForbiddenProblemDetailsHandler.cs
new file mode 100644
index 0000000..23d2eab
--- /dev/null
+++ b/src/api/SIGCM2.Api/Authorization/ForbiddenProblemDetailsHandler.cs
@@ -0,0 +1,58 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Policy;
+
+namespace SIGCM2.Api.Authorization;
+
+///
+/// Custom IAuthorizationMiddlewareResultHandler that emits a structured ProblemDetails
+/// response for 403 Forbidden outcomes (authenticated user, missing permission).
+///
+/// For 401 Unauthorized and successful outcomes, delegates to the default handler
+/// so the existing JWT Bearer challenge flow is unaffected (REQ-B-07).
+///
+/// Registered as singleton in Program.cs — depends only on framework services.
+///
+public sealed class ForbiddenProblemDetailsHandler : IAuthorizationMiddlewareResultHandler
+{
+ private static readonly AuthorizationMiddlewareResultHandler DefaultHandler = new();
+
+ private static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ public async Task HandleAsync(
+ RequestDelegate next,
+ HttpContext context,
+ AuthorizationPolicy policy,
+ PolicyAuthorizationResult authorizeResult)
+ {
+ // Only intercept 403s for authenticated users.
+ // If the user is not authenticated, the 401 challenge is handled by JwtBearer (REQ-B-07).
+ if (authorizeResult.Forbidden && context.User.Identity?.IsAuthenticated == true)
+ {
+ var requiredPermission = context.Items["RequiredPermission"] as string;
+
+ context.Response.StatusCode = StatusCodes.Status403Forbidden;
+ context.Response.ContentType = "application/problem+json; charset=utf-8";
+
+ var problem = new
+ {
+ type = "https://sigcm2.local/errors/forbidden",
+ title = "Acceso denegado",
+ status = 403,
+ detail = "No tenés el permiso requerido para ejecutar esta acción.",
+ permisoRequerido = requiredPermission,
+ };
+
+ await context.Response.WriteAsync(
+ JsonSerializer.Serialize(problem, SerializerOptions));
+
+ return;
+ }
+
+ // Delegate 401 challenges and successful outcomes to the default handler
+ await DefaultHandler.HandleAsync(next, context, policy, authorizeResult);
+ }
+}
diff --git a/src/api/SIGCM2.Api/Program.cs b/src/api/SIGCM2.Api/Program.cs
index 545deed..313fe60 100644
--- a/src/api/SIGCM2.Api/Program.cs
+++ b/src/api/SIGCM2.Api/Program.cs
@@ -26,6 +26,7 @@ builder.Services.AddInfrastructure(builder.Configuration);
// Authorization — handler lives in Api layer; DO NOT move to Infrastructure DI
builder.Services.AddAuthorization();
builder.Services.AddScoped();
+builder.Services.AddSingleton();
// Controllers with exception filter
builder.Services.AddControllers(opts =>