How to change status code & add message from failed AuthorizationHandler policy
Asked Answered
M

6

14

Working on a .net core app implementing a custom policy.

Let's say we have a very simple custom policy:

internal class RequireNamePolicy : AuthorizationHandler<RequireNameRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNameRequirement requirement)
    {
        var nameClaim = context.User.Claims.FirstOrDefault(c => c.Type == Claims.Name);
        if (nameClaim != null && nameClaim.Value == "Chimney Spork")
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

internal class RequireNameRequirement : IAuthorizationRequirement
{

}

Now let's say the claim doesn't exist, so we hit context.Fail(). The default response is a 403 with no message body.

My question is, where would we change the status code (to 401) and return a message that states the problem (ie claim not present)?

Moire answered 20/2, 2018 at 16:19 Comment(0)
G
-1

This is not yet implemented. You can follow up on this here. One possible workaround would be:

internal class RequireNamePolicy : AuthorizationHandler<RequireNameRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNameRequirement requirement)
    {
        var authorizationFilterContext = context.Resource as AuthorizationFilterContext;

        var nameClaim = context.User.Claims.FirstOrDefault(c => c.Type == Claims.Name);
        if (nameClaim != null && nameClaim.Value == "Chimney Spork")
        {
            context.Succeed(requirement);
        }
        else
        {
            authorizationFilterContext.Result = new JsonResult("Custom message") { StatusCode = 401 };
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Gadmon answered 19/3, 2019 at 6:40 Comment(2)
This seems to be working, but looks ugly as hell. ThanksLiterature
It returns 200 and it failsSiret
E
12

context.Resource as AuthorizationFilterContext is null in net core 3.1

Finally I rewrite the method to this:

  public class SysUserAuthHandler : AuthorizationHandler<SysUserAuthRequirement> {

    private readonly IFetchLoginUser fetchUser;

    private readonly IHttpContextAccessor httpContextAccessor;

    public SysUserAuthHandler( IFetchLoginUser fetchLoginUser, IHttpContextAccessor httpContextAccessor ) {
      fetchUser = fetchLoginUser;
      this.httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, SysUserAuthRequirement requirement ) {
      var httpContext = httpContextAccessor.HttpContext;
      byte[] bytes;
      string msg;

      if (!string.IsNullOrWhiteSpace( context.User.Identity.Name )) {
        var myUser = fetchUser.LoadUser( context.User.Identity.Name, SystemEnum.FooSytem);

        if ((myUser.Auth & requirement.Auth) == requirement.Auth) {
          context.Succeed( requirement );
          return Task.CompletedTask;
        }

        msg = requirement.Auth switch {
          1 => "You don't have Auth of Maker",
          2 => "You don't have Auth of Checker",
          4 => "You don't have Auth of Admin",
          8 => "You don't have Auth of Operator",
          _ => "You don't have Auth"
        };
      }
      else {
        msg = "User Invalid, Please check your login status or login again";
      }

      bytes = Encoding.UTF8.GetBytes( msg );
      httpContext.Response.StatusCode = 405;
      httpContext.Response.ContentType = "application/json";
      httpContext.Response.Body.WriteAsync( bytes, 0, bytes.Length );
      //context.Succeed( requirement );
      return Task.CompletedTask;
    }

  }

  public class SysUserAuthRequirement : IAuthorizationRequirement {

    public long Auth { get; private set; }

    public SysUserAuthRequirement( long auth ) {
      Auth = auth;
    }

  }

Do not forget add this line in Startup

    services.AddHttpContextAccessor();
Excited answered 18/5, 2020 at 1:32 Comment(1)
Please note: If your own HandleRequirementAsync causes net::ERR_HTTP2_PROTOCOL_ERROR in Chrome then look at #74450033Corettacorette
K
10

The documentation for Customize the behavior of AuthorizationMiddleware can be found below:

https://learn.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0

My code finally looked like this:

public class GuidKeyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler
         DefaultHandler = new AuthorizationMiddlewareResultHandler();

    public async Task HandleAsync(
        RequestDelegate requestDelegate,
        HttpContext httpContext,
        AuthorizationPolicy authorizationPolicy,
        PolicyAuthorizationResult policyAuthorizationResult)
    {

        if (policyAuthorizationResult.Challenged && !policyAuthorizationResult.Succeeded && authorizationPolicy.Requirements.Any(requirement => requirement is GuidKeyRequirement))
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            return;
        }

        // Fallback to the default implementation.
        await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy,
                               policyAuthorizationResult);
    }
}

Startup.cs:

services.AddSingleton<IAuthorizationMiddlewareResultHandler,
    GuidKeyAuthorizationMiddlewareResultHandler>();
Kele answered 18/8, 2021 at 15:17 Comment(1)
IAuthorizationMiddlewareResultHandler is new in asp.net 5 !Laudian
P
1

I have achieved it using ExceptionHandler. I wanted to keep 403 status code, but to customize error message. So I have created custom Exception:

public class ForbiddenException : BaseException
{
    public ForbiddenException(string message) : base(HttpStatusCode.Forbidden, message)
    {
    }

    public ForbiddenException(string message, object info) : base(HttpStatusCode.Forbidden, message, info)
    {
    }
}

public abstract class BaseException : Exception
{
    protected BaseException(HttpStatusCode httpErrorCode, string message, Exception innerException = null) : base(message,
        innerException)
    {
        HttpErrorCode = httpErrorCode;
    }

    protected BaseException(HttpStatusCode httpErrorCode, string message, object info, Exception innerException = null) :
        this(httpErrorCode,
            message, innerException)
    {
        Info = info;
    }

    public HttpStatusCode HttpErrorCode { get; set; }

    public object Info { get; set; }
}

Then inside AuthorizationHandler I threw this exception:

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        List<Claim> userPermissionsClaims =
            context.User.Claims.Where(c => c.Type == PermissionConstants.PermissionClaimType).ToList();

        if (userPermissionsClaims.Any(pc => requirement.Permissions.Any(p => (Permissions)Convert.ToInt16(pc.Value) == p)))
        {
            context.Succeed(requirement);
        }
        else
        {
            throw new ForbiddenException(string.Format(ErrorMessages.PermissionsRequired, string.Join(", ", requirement.Permissions)));
        }

        return Task.CompletedTask;
    }
}

Here is a global exception handler:

app.UseExceptionHandler(exceptionHandler =>
        {
            exceptionHandler.Run(async context =>
            {
                await HandleException(context.Features.Get<IExceptionHandlerFeature>().Error, context, env);
            });
        });

public async Task HandleException(Exception exception, HttpContext context)
    {
        var message = exception?.Message;

        if (exception is BaseException)
        {
            context.Response.StatusCode = (int)(exception as BaseException)?.HttpErrorCode;
        }
        else
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        }

        await context.Response.WriteAsync(message);
        await context.Response.CompleteAsync();
    }
Peroration answered 21/9, 2021 at 8:52 Comment(0)
A
1

The ones that work above for asp.netcore 3.1 failed on my integration tests, but Postman worked. I ended up doing something similar to the following:

public class MyAuthorizationHandler: AuthorizationHandler<PermissionRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MyAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        //code to check for authorization
        if (isAuthorized)
        {
            context.Succeed(requirement);
        }
        else
        {
            var message = "Custom message";
            _httpContextAccessor.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            _httpContextAccessor.HttpContext.Response.ContentType = "application/json";
            await _httpContextAccessor.HttpContext.Response.WriteAsync(message);
            await _httpContextAccessor.HttpContext.Response.CompleteAsync(); //Needed this or my integration tests failed with a System.IO exception.
            context.Fail();
        }
    }
}

internal class PermissionRequirement : IAuthorizationRequirement
{

}

Update Turns out even though the above code executed correctly in Postman tests and Integration tests passed, there were Splunk errors being logged: "System.InvalidOperationException: StatusCode cannot be set because the response has already started." I ended up upgrading to .net 5.0 and using the Middleware Result Handler instead. https://learn.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0. The code used to create the message was similar, except CompleteAsync() was not needed.

Ashlieashlin answered 6/11, 2021 at 1:41 Comment(0)
K
0

Set ITempDataProvider and IHttpContextAccessor with Dependency Injection.

Save message in HandleRequirementAsync method:

Dictionary<string, object> errs = new Dictionary<string, object>();
errs.Add("AccessDeniedReason", "The current domain is not enabled!");
                
TempDataProvider.SaveTempData(HttpContextAccessor.HttpContext, errs);
context.Fail();

Load message in View:

@page
@model AccessDeniedModel
@inject ITempDataProvider TempDataProvider

@{
var t =  TempDataProvider.LoadTempData(HttpContext);
string AccessDeniedReason = (string)t?["AccessDeniedReason"];

// ...
}
Kimber answered 4/10, 2021 at 11:35 Comment(0)
G
-1

This is not yet implemented. You can follow up on this here. One possible workaround would be:

internal class RequireNamePolicy : AuthorizationHandler<RequireNameRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNameRequirement requirement)
    {
        var authorizationFilterContext = context.Resource as AuthorizationFilterContext;

        var nameClaim = context.User.Claims.FirstOrDefault(c => c.Type == Claims.Name);
        if (nameClaim != null && nameClaim.Value == "Chimney Spork")
        {
            context.Succeed(requirement);
        }
        else
        {
            authorizationFilterContext.Result = new JsonResult("Custom message") { StatusCode = 401 };
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Gadmon answered 19/3, 2019 at 6:40 Comment(2)
This seems to be working, but looks ugly as hell. ThanksLiterature
It returns 200 and it failsSiret

© 2022 - 2024 — McMap. All rights reserved.