.net core - How to return 403 on AuthorizationHandler? - api

I implemented my custom AuthorizationHandler.
On that i check i the user can resolved and is active.
If the user isn't active then i would like to return an 403 status.
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserRequirement requirement)
{
var userId = context.User.FindFirstValue( ClaimTypes.NameIdentifier );
if (userId != null)
{
var user = await _userManager.GetUserAsync(userId);
if (user != null)
{
_httpContextAccessor.HttpContext.AddCurrentUser(user);
if (user.Active)
{
context.Succeed(requirement);
return;
}
else
{
_log.LogWarning(string.Format("User ´{1}´ with id: ´{0} isn't active", userId, user.UserName), null);
}
}
else
{
_log.LogWarning(string.Format("Can't find user with id: ´{0}´", userId), null);
}
} else
{
_log.LogWarning(string.Format("Can't get user id from token"), null);
}
context.Fail();
var response = _httpContextAccessor.HttpContext.Response;
response.StatusCode = 403;
}
But i receive a 401. Can you please help me?

Could you check that on the end of your function? I'm using that in my custom middleware to rewrite status code to 401 in some cases but in your scenario should also work
var filterContext = context.Resource as AuthorizationFilterContext;
var response = filterContext?.HttpContext.Response;
response?.OnStarting(async () =>
{
filterContext.HttpContext.Response.StatusCode = 403;
//await response.Body.WriteAsync(message, 0, message.Length); only when you want to pass a message
});

According to the Single Responsibility Principle , we should not use the HandleRequirementAsync() method to redirect reponse , we should use middleware or Controller to do that instead . If you put the redirect logic in HandleRequirementAsync() , how about if you want to use it in View page ?
You can remove the redirection-related code to somewhere else (outside) , and now you inject an IAuthorizationService to authorize anything as you like , even a resource-based authorization :
public class YourController : Controller{
private readonly IAuthorizationService _authorizationService;
public YourController(IAuthorizationService authorizationService)
{
this._authorizationService = authorizationService;
}
[Authorize("YYY")]
public async Task<IActionResult> Index()
{
var resource /* = ... */ ;
var x = await this._authorizationService.AuthorizeAsync(User,resource , "UserNameActiveCheck");
if (x.Succeeded)
{
return View();
}
else {
return new StatusCodeResult(403);
}
}
}

in .NET core 6.0 you can use the Fail method
AuthorizationHandlerContext.Fail Method
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AppAuthorizationRequirement requirement)
{
context.Fail(); //Use this
}

Related

Asp.Net AuthorizationHandler's response upsets Chrome, causes "net::ERR_HTTP2_PROTOCOL_ERROR"

I've decided to write a custom AuthorizationHandler for a custom Policy I'm using :
// I pass this to AddPolicy in startup.cs
public class MyRequirement : IAuthorizationRequirement {
public MyRequirement () { ... }
}
public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement> {
public MyAuthorizationHandler() { }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {
if (context.Resource is HttpContext httpContext) {
var endpoint = httpContext.GetEndpoint();
if ( /* conditions for hard failure */ ) { context.Fail(); return; }
if ( /* conditions for success */) { context.Succeed(requirement); return; }
// Neither a success nor a failure, simply a different response.
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);
return;
}
context.Fail();
}
}
I've seen similar code snippets in other StackOverlflow answers. (e.g. here : How to change status code & add message from failed AuthorizationHandler policy )
Problem : this doesn't seem to generate a "valid" 404 response.
I think so for two reasons:
When I look at Chrome's network tab, the response is NOT "404", instead it's net::ERR_HTTP2_PROTOCOL_ERROR 404
When I look at the response data, there's only headers. My custom error text ("Blah blah NotFound") does not appear anywhere.
What am I doing wrong?
Note : I've tried returning immediately after setting the 404, without doing context.Fail() but I get the same result.
The root cause:
My Web Api had several middlewares working with the response value. Those middleware were chained up in Startup.cs, using the traditional app.UseXXX().
Chrome was receiving 404 (along with my custom response body) from my Requirement middleware (hurray!), but Chrome is "smart" and by design continues to receive the response even after that initial 404 -- for as long as the server continues generating some response data.
Because of that, Chrome eventually came across a different response added by another of the chained up middlewares. The 404 was still there, but the response body was slightly changed.
And since chrome is paranoid, it would display this net::ERR_HTTP2_PROTOCOL_ERROR to indicate that someone had messed up the consistency of the response somewhere along the chain of responders.
==========
The solution :
Finalize your response with Response.CompleteAsync() to prevent any other subsequent middleware from changing it further :
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {
if (context.Resource is HttpContext httpContext) {
var endpoint = httpContext.GetEndpoint();
if ( /* conditions for hard failure */ ) { context.Fail(); return; }
if ( /* conditions for success */) {
context.Succeed(requirement);
return;
}
// Neither a requirement success nor a requirement failure, just a different response :
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync("Blah blah NotFound");
await httpContext.Response.CompleteAsync(); // <-- THIS!!!
return;
}
context.Fail();
}
Please note : if your 'HandleRequirementAsync' function does not have the 'async' keyword, then do not use 'await' inside of it, and do return Task.CompletedTask; instead of just return;
Below is a work demo based on your code, you can refer to it.
public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement>
{
private readonly IHttpContextAccessor httpContextAccessor;
public MyAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement)
{
if ( /* conditions for success */) {
context.Succeed(requirement);
return;
}
// If it fails at this point, I want to return 404 because of reasons.
else
{
var httpContext = httpContextAccessor.HttpContext;
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);
}
return Task.CompletedTask;
}
}
result:

Postman returning not found 404 in api of .net core

net core 3.1, on the post method, postman returns status 404 not found. The commented code is what I tried.
[Route("api/Servicio")]
public class ServicioController : Controller
private readonly ApplicationDbContext _context;
public ServicioController(ApplicationDbContext context)
{
_context = context;
}
// POST: api/PostServicio
//[HttpPost("api/PostServicio")]
//[HttpPost("servicio")]
[HttpPost("api/PostServicio/{servicio}")]
//public async Task<ActionResult<Servicio>> PostServicio([FromBody]Servicio servicio)
public async Task<ActionResult<Servicio>> PostServicio(Servicio servicio)
{
_context.Servicio.Add(servicio);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetServicio), new { id = servicio.Id }, servicio);
}
// GET: api/GetServicio/5
[HttpGet("GetServicio/{tecnico}/{semanaDelAno}")]
public async Task<ActionResult<Servicio>> GetServicio(string tecnico, int semanaDelAno)
{
var servicio = await _context.Servicio.FirstOrDefaultAsync(i => i.Tecnico == tecnico &&
i.SemanaDelAno == semanaDelAno);
if (servicio == null)
{
return NotFound();
}
return servicio;
}
}
In postman i have a body, raw, json
{
"Tecnico":"Jhon",
"ServicioRealizado":"Servicio1",
"SemanaDelAno": 1,
"Dia": "Lunes",
"HoraInicial": 13.0,
"HoraFinal": 15.0
}
I have this two actions, I got this code basically from the api tutorial in the documentation.
UPDATE ************************************************
// POST: api/PostServicio
[HttpPost("PostServicio")]
public async Task<ActionResult<Servicio>> PostServicio([FromBody]Servicio servicio)
{
_context.Servicio.Add(servicio);
await _context.SaveChangesAsync();
//return CreatedAtAction(nameof(GetServicio), new { tecnico = servicio.Tecnico }, new { semanaDelAno = servicio.SemanaDelAno });
return servicio;
}
Now it works
Be sure your request url is:https://localhost:portNumber/api/Servicio/api/PostServicio/xxx.xxx matches the {servicio}.
But actually I think it is no need add {servicio} to your HttpGet attribute.Because you post the data from body instead of route.So the {servicio} here is useless.Just use [HttpPost("api/PostServicio")] and the request url:https://localhost:portNumber/api/Servicio/api/PostServicio.Then post the data from body by choose raw json in postman.
Reference:
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#attribute-routing-with-http-verb-attributes

How to allow multiple roles to access route through RouteClaimsRequirement

In a regular type scenario, where a Route is available, say to only "Premium" users, ocelot.global.json would have RouteClaimsRequirement like this:
"RouteClaimsRequirement" : { "Role" : "Premium" }
This would get translated to a KeyValuePair<string, string>(), and it works nicely.
However, if I were to open a route to 2 types of users, eg. "Regular" and "Premium", how exactly could I achieve this?
I found a way through overriding of default Ocelot middleware. Here are some useful code snippets:
First, override the default AuthorizationMiddleware in Configuration() in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var config = new OcelotPipelineConfiguration
{
AuthorisationMiddleware
= async (downStreamContext, next) =>
await OcelotJwtMiddleware.CreateAuthorizationFilter(downStreamContext, next)
};
app.UseOcelot(config).Wait();
}
As you can see, I am using a custom OcelotJwtMiddleware class up there. Here is that class, pasted:
public static class OcelotJwtMiddleware
{
private static readonly string RoleSeparator = ",";
public static Func<DownstreamContext, Func<Task>, Task> CreateAuthorizationFilter
=> async (downStreamContext, next) =>
{
HttpContext httpContext = downStreamContext.HttpContext;
var token = httpContext.Request.Cookies[JwtManager.AuthorizationTokenKey];
if (token != null && AuthorizeIfValidToken(downStreamContext, token))
{
await next.Invoke();
}
else
{
downStreamContext.DownstreamResponse =
new DownstreamResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
};
private static bool AuthorizeIfValidToken(DownstreamContext downStreamContext, string jwtToken)
{
IIdentityProvider decodedObject = new JwtManager().Decode<UserToken>(jwtToken);
if (decodedObject != null)
{
return downStreamContext.DownstreamReRoute.RouteClaimsRequirement["Role"]
?.Split(RoleSeparator)
.FirstOrDefault(role => role.Trim() == decodedObject.GetRole()) != default;
}
return false;
}
}
JwtManager class here is just my small utility made using the default Jwt NuGet package, nothing special. Also, JWT is being stored as a Cookie, which is not safe, but doesn't matter here. If you happen to copy paste your code, you will have small errors relating to this, but just switch it out with your own implementations of auth tokens.
After these 2 snippets were implemented, ocelot.global.json can have RouteClaimsRequirement such as this:
"RouteClaimsRequirement" : { "Role" : "Premium, Regular" }
This will recognize both clients with Regular in their Cookies, as well as those with Premium.

How to return a Json object error on ASP.Net Core Restful Cotroller?

I have a ASP.NET controller that controls a schedule (as I'm Brazilian, schedule in Portuguese means Agendamento).
The thing is, I can't allow scheduling the same room (in Portuguese Sala) being taken twice at the same time.
So in the POST request I check the DB to see if that room has already being taken and if it has I want to return only a Json object { "error": "You can't do that." }.
If the request does not have any problem then the insert should be done and the inserted object has to be returned.
[HttpPost]
public async Task<ActionResult<Agendamento>> PostAgendamento(Agendamento agendamento)
{
var agendamentosJaExistentes = await _context.Agendamentos.Include(ag => ag.Sala)
.Where(ag =>
ag.SalaId == agendamento.SalaId &&
(
(agendamento.PeriodoInicial >= ag.PeriodoInicial && agendamento.PeriodoInicial <= ag.PeriodoFinal)
||
(agendamento.PeriodoFinal >= ag.PeriodoInicial && agendamento.PeriodoFinal <= ag.PeriodoFinal)
))
.ToListAsync();
if (agendamentosJaExistentes != null)
{
return ??? JSON OBJECT ???
}
_context.Agendamentos.Add(agendamento);
await _context.SaveChangesAsync();
return CreatedAtAction("GetAgendamento", new { id = agendamento.Id }, agendamento);
}
Can you guys help me?
Add NewtonsoftJson support
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson();
}
Return JsonObject
if (agendamentosJaExistentes != null)
{
return new ObjectResult(Error("You can't do that.")); //???JSON OBJECT???
}
400 Bad Request response status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error.
public class ReturnJson
{
public string Status { get; set; }
public string Message { get; set; }
}
public static ReturnJson Error(string responseMessage, string responseCode = "400")
{
ReturnJson returnJson = new ReturnJson()
{
Status = responseCode,
Message = responseMessage ?? string.Empty
};
return returnJson;
}
Test Result:
For RESTful api one of the best practice is to return errors aligning with HTTP status code. For this specific case probably HTTP 500 (instead of 200 success with an error body) makes more sense.
You can do so by this:
var result = new
{
Error = "Room already booked",
};
return this.StatusCode(StatusCodes.Status500InternalServerError, result);
This is a simple way that am using (DoteNet Core 3.x)
return Json(new { error = "Your error message " , status = 405 });

What is the best possible way to send custom error responses in .net core web api

I'm making a .net Core WebApi using .Net Core 2.2. The API is ready but the failure message and response is where I'm stuck at.
Right now, I'm getting respose like below
json
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
I created a json formatter class and wrote the below code
public class SuperJsonOutputFormatter : JsonOutputFormatter
{
public SuperJsonOutputFormatter(
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool) : base(serializerSettings, charPool)
{
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null)
throw new ArgumentNullException(nameof(selectedEncoding));
using (TextWriter writer =
context.WriterFactory(
context.HttpContext.Response.Body,
selectedEncoding))
{
var rewrittenValue = new
{
resultCode = context.HttpContext.Response.StatusCode,
resultMessage =
((HttpStatusCode)context.HttpContext.Response.StatusCode)
.ToString(),
result = context.Object
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
}
I expect all the error codes to be sent as generic error messages like the JSON below.
FOR STATUS OKAY:
{
"status" : True,
"error" : null,
"data" : {
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
}
}
FOR OTHER STATUS LIKE 404, 500, 400, 204
{
"status" : False,
"error" : {
"error code" : 404,
"error description" : Not Found
},
"data" : null
}
I expect all the error codes to be sent as generic error messages like the JSON below
You're almost there. What you need to do is enabling your SuperJsonOutputFormatter.
A Little Change to Your Formatter
Firstly, your formatter didn't return a json with the same schema as you want. So I create a dummy class to hold the information for error code and error description:
public class ErrorDescription{
public ErrorDescription(HttpStatusCode statusCode)
{
this.Code = (int)statusCode;
this.Description = statusCode.ToString();
}
[JsonProperty("error code")]
public int Code {get;set;}
[JsonProperty("error description")]
public string Description {get;set;}
}
And change your WriteResponseBodyAsync() method as below:
...
using (TextWriter writer = context.WriterFactory(context.HttpContext.Response.Body, selectedEncoding)) {
var statusCode = context.HttpContext.Response.StatusCode;
var rewrittenValue = new {
status = IsSucceeded(statusCode),
error = IsSucceeded(statusCode) ? null : new ErrorDescription((HttpStatusCode)statusCode),
data = context.Object,
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
Here the IsSucceeded(statusCode) is a simple helper method that you can custom as you need:
private bool IsSucceeded(int statusCode){
// I don't think 204 indicates that's an error.
// However, you could comment out it if you like
if(statusCode >= 400 /* || statusCode==204 */ ) { return false; }
return true;
}
Enable your Formatter
Secondly, to enable your custom Formatter, you have two approaches: One way is to register it as an global Formatter, the other way is to enable it for particular Controller or Action. Personally, I believe the 2nd way is better. So I create a Action Filter to enable your formatter.
Here's an implementation of the Filter that enables your custom formatter dynamically:
public class SuperJsonOutputFormatterFilter : IAsyncActionFilter{
private readonly SuperJsonOutputFormatter _formatter;
// inject your SuperJsonOutputFormatter service
public SuperJsonOutputFormatterFilter(SuperJsonOutputFormatter formatter){
this._formatter = formatter;
}
// a helper method that provides an ObjectResult wrapper over the raw object
private ObjectResult WrapObjectResult(ActionExecutedContext context, object obj){
var wrapper = new ObjectResult(obj);
wrapper.Formatters.Add(this._formatter);
context.Result= wrapper;
return wrapper;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ActionExecutedContext resultContext = await next();
// in case we get a 500
if(resultContext.Exception != null && ! resultContext.ExceptionHandled){
var ewrapper = this.WrapObjectResult(resultContext, new {});
ewrapper.StatusCode = (int) HttpStatusCode.InternalServerError;
resultContext.ExceptionHandled = true;
return;
}
else {
switch(resultContext.Result){
case BadRequestObjectResult b : // 400 with an object
var bwrapper=this.WrapObjectResult(resultContext,b.Value);
bwrapper.StatusCode = b.StatusCode;
break;
case NotFoundObjectResult n : // 404 with an object
var nwrapper=this.WrapObjectResult(resultContext,n.Value);
nwrapper.StatusCode = n.StatusCode;
break;
case ObjectResult o : // plain object
this.WrapObjectResult(resultContext,o.Value);
break;
case JsonResult j : // plain json
this.WrapObjectResult(resultContext,j.Value);
break;
case StatusCodeResult s: // other statusCodeResult(including NotFound,NoContent,...), you might want to custom this case
var swrapper = this.WrapObjectResult(resultContext, new {});
swrapper.StatusCode = s.StatusCode;
break;
}
}
}
}
And don't forget to register your formatter as a service :
services.AddScoped<SuperJsonOutputFormatter>();
Finally, when you want to enable your formatter, just add a [TypeFilter(typeof(SuperJsonOutputFormatterFilter))] annotation for the controller or action.
Demo
Let's create an action method for Test:
[TypeFilter(typeof(SuperJsonOutputFormatterFilter))]
public IActionResult Test(int status)
{
// test json result(200)
if(status == 200){ return Json(new { Id = 1, }); }
// test 400 object result
else if(status == 400){ return BadRequest( new {}); }
// test 404 object result
else if(status == 404){ return NotFound(new { Id = 1, }); }
// test exception
else if(status == 500){ throw new Exception("unexpected exception"); }
// test status code result
else if(status == 204){ return new StatusCodeResult(204); }
// test normal object result(200)
var raw = new ObjectResult(new XModel{
empId=1999,
empName = "Conroy, Deborah",
enrollmentStatus=true,
primaryFingerprintScore=65,
secondaryFingerprintScore=60,
primaryFingerprint = null,
secondaryFingerprint= null,
primaryFingerprintType=null,
secondaryFingerprintType=null
});
return raw;
}
Screenshot: