1. The goal
The goal is to read the ResponseFailureReason and have a custom dto response sent with the failure message to the caller if the AuthorizationHandler fails to evaluate the Policy for for a specific requirement for example if a permission (which is stored in a Database) is declined for a certain role then the api should return my custom ExceptionDto as a response as maybe together with a status 403.
The reason I want it like this is because I want to have one specific handler to check the database if it has any permissions for a specific role and controller/action endpoint.
2. The current code setup
I've added a rich EndpointService System which scans all controllers and endpoints with every API startup and it manages all the endpoints from the api in a database. I also added an entity named CAERolePermissionModel
containing a relation with a certain role defined and which also contains the name of both the controller and the action endpoint so that I can check on a request if a certain user with a certain role has permission to a certain controller endpoint or action endpoint which sits the given controller.
I've also added an AuthorizationHandler as a Transient service to the DI Container and added the AuthorizationHandler (via Startup.cs) to use the requirement needed by the policy. I further then added the policy's name to the Controller's Authorize Attribute.
3. Some code bro
3.1 Startup.cs
// ... code removed for brevity ...
//***************************************
// Register the DI Container *
//***************************************
services.AddTransient<IAuthorizationHandler, MyPermissionHandler>();
//***************************************
// Authorization *
//***************************************
services.AddAuthorization(options =>
{
options.AddPolicy("PermissionGranterPolicy", policy =>
policy.Requirements.Add(new MyPermissionRequirement()));
});
// ... code removed for brevity ...
3.2. MyPermissionRequirement.cs
public class MyPermissionRequirement : IAuthorizationRequirement { }
3.3. MyPermissionHandler.cs
public class MyPermissionHandler : AuthorizationHandler<MyPermissionRequirement>
{
public MyPermissionHandler(
MyDBContext dBContext,
IUserService userService,
ICAERolePermissionService rolePermissionService
)
{
_dBContext = dBContext;
_userService = userService;
_rolePermissionService = rolePermissionService;
}
private readonly MyDBContext _dBContext;
private readonly IUserService _userService;
private readonly ICAERolePermissionService _rolePermissionService;
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MyPermissionRequirement requirement
)
{
if (context.Resource is HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
var controllerName = actionDescriptor.ControllerName;
var actionName = actionDescriptor.ActionName;
try
{
if (!_userService.IsAuthenticated)
{
throw new PermissionDeniedException("No authentication found!");
}
if(!_rolePermissionService.HasPermission(
_userService.DbUser,
controllerName,
actionName
))
{
throw new PermissionDeniedException($"User '{_userService.DbUser.UserName}' has no permission for [{controllerName}]->{actionName}");
}
context.Succeed(requirement);
}
catch (PermissionDeniedException ex)
{
// How and where can I read the AuthorizationFailureReason Message?
context.Fail(new AuthorizationFailureReason(this, ex.Message));
}
await Task.CompletedTask;
}
}
}
3.4. UserController.cs
// Note : (Needs the "PermissionGranterPolicy" to evaluate the permission)
[EnableCors("CorsPolicy")]
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "PermissionGranterPolicy")]
public class UserController : Controller
{
[HttpGet]
public ActionResult<bool> Get()
{
return Ok(true);
}
}
4. Tries to resolve my Issue
The following is a list of things I've tried but couldn't get happy with the approach (or furthermore the solution) in order to get the custom response dto out to the caller.
4.1. Tried using a custom ExceptionHandlerMiddleware
Tried to rethrow the exception in the handler and catch it with a global intercepting ExceptionHandlerMiddleware
(Source: NET 6.0 - Global Error Handler Tutorial with Example) which seemed to work, it was possible to send out the custom exceptiondto. But that would mean to catch all Exceptions, within this intercepting handler, which are not specifically handled in a controller and I wasn't happy with at all with this approach because wouldn't I be forced to handle every exception in this ExceptionHandler? And what I've figured from the Exception handler lambda Docs over at Microsoft this should also just be used in the production environment since the developer environment has its own ways of dealing with exceptions by using app.UseDeveloperExceptionPage();
in the Startup.cs file.
4.2. Tried using an IAuthorizationMiddlewareResultHandler
Tried to use an IAuthorizationMiddlewareResultHandler
(Sources: Customize the behavior of AuthorizationMiddleware | and as suggested by Ogglas here on this thread) trying to read the AuthorizationFailureReason
in the HandleAsync
Method by reading the PolicyAuthorizationResult authorizeResult
parameter but sadly I couldn't find my AuthorizationFailureReason's Message anywhere (also not in the other parameters). And secondly this IAuthorizationMiddlewareResultHandler
intercepted every Authorize behaviour even the one's that didn't use my policy named PermissionGranterPolicy
.
4.3. Tried reading the failure reason from UseStatusCodePages with lambda
Tried to read the AuthorizationFailureReason
by using UseStatusCodePages
(Source: UseStatusCodePages with lambda) but same as in case 2 from this list I couldn't find the FailureReason.
4.4. Tried to directly write a response from the MyPermissionHandlers catch block
Tried to directly write a response from the MyPermissionHandler.cs
in the catch block by writing with await response.WriteAsync(jsonSerializedExceptionDto)
and calling await response.CompleteAsync()
. ODD IS: The dto gets sent out and I receive the contents of the dto but in Visual Studio 2022 I get two huge exceptions in the output telling me the StatusCode can not be set because the response has already started. And if I comment out the app.UseDeveloperExceptionPage();
line one error seems to disappear but there is still the other error which I couldn't trace it's origin. Trying to trace it I commented/uncommented every line in the Startup.cs which seems to handle some sort of authorization, authentication or cors behaviours but without success. Also checking the response.IsStarted
Property and only executing the WriteAsync
Method when the IsStarted
is set to false within my AuthorizationHandler didn't resolve the issue and the exceptions were still being thrown. Which leads me to the next chapter of this question with many ... (read the title below to complete this sentence)
5. Questions
Where would I want to handle the
AuthorizationFailureReason
and how can I retrieve the reason message from theAuthorizationFailureReason
?Where is the right and best place (which middleware, service etc?) to retrieve the message from
AuthorizationFailureReason
and send the custom exceptiondto usingWriteAsync
?As in chapter 4.1. of this question: Can it be that these two exceptions are only outputted in development mode? (I am asking since my production environment is very heavy limited and sadly I can't test to log these exceptions for example by using serilog or some other output mechanism).
6. Last Words
I hope my question makes clear what I'm looking for and as previously stated I am surely missing something and I was now hoping to find some answers here on Stackoverflow.
Any kind of input on this is as always highly appreciated