feat(api): ForbiddenProblemDetailsHandler 403 shape [UDT-006]
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Authorization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
|||||||
// Authorization — handler lives in Api layer; DO NOT move to Infrastructure DI
|
// Authorization — handler lives in Api layer; DO NOT move to Infrastructure DI
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
|
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenProblemDetailsHandler>();
|
||||||
|
|
||||||
// Controllers with exception filter
|
// Controllers with exception filter
|
||||||
builder.Services.AddControllers(opts =>
|
builder.Services.AddControllers(opts =>
|
||||||
|
|||||||
Reference in New Issue
Block a user