Return HTTP 403 using Authorize attribute in ASP.Net Core

I ended up doing it with middleware:

public class AuthorizeCorrectlyMiddleware
{
    readonly RequestDelegate next;

    public AuthorizeCorrectlyMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await next(context);

        if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
        {
            if (context.User.Identity.IsAuthenticated)
            {
                //the user is authenticated, yet we are returning a 401
                //let's return a 403 instead
                context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            }
        }
    }
}

which should be registered in Startup.Configure before calling app.UseMvc().


I followed the guide for Custom Authorization Policy Providers using IAuthorizationPolicyProvider in ASP.NET Core and also wanted to create a custom response.

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

The guide I followed for that was Customize the behavior of AuthorizationMiddleware

https://docs.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>();

You can also edit your AuthorizationHandler and access httpContext via IHttpContextAccessor. However this feels more like a hack.

internal class GuidKeyAuthorizationHandler : AuthorizationHandler<GuidKeyRequirement>
{
    private readonly ILogger<GuidKeyAuthorizationHandler> _logger;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public GuidKeyAuthorizationHandler(ILogger<GuidKeyAuthorizationHandler> logger, IHttpContextAccessor httpContextAccessor)
    {
        _logger = logger;
        _httpContextAccessor = httpContextAccessor;
    }

    // Check whether a given GuidKeyRequirement is satisfied or not for a particular context
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, GuidKeyRequirement requirement)
    {
        var httpContext = _httpContextAccessor.HttpContext; // Access context here

        var key = System.Web.HttpUtility.ParseQueryString(httpContext.Request.QueryString.Value).Get("key");

        if (!string.IsNullOrWhiteSpace(key))
        {
            // If the user guid key matches mark the authorization requirement succeeded
            if (Guid.TryParse(key, out var guidKey) && guidKey == requirement.Key)
            {
                _logger.LogInformation("Guid key is correct");

                if (requirement.RequireRefererHeader)
                {
                    _logger.LogInformation("Require correct referer header");
                    httpContext.Request.Headers.TryGetValue("Referer", out var refererHeader);
                    if (requirement.RefererHeader == refererHeader)
                    {
                        _logger.LogInformation("Referer header is correct");
                        context.Succeed(requirement);
                        return Task.CompletedTask;
                    }
                    else
                    {
                        _logger.LogInformation($"Referer header {refererHeader} is not correct");
                    }
                }
                else
                {
                    _logger.LogInformation("Correct referer header is not needed");
                    context.Succeed(requirement);
                    return Task.CompletedTask;
                }
            }
            else
            {
                _logger.LogInformation($"Guid key {guidKey} is not correct");
            }
        }
        else
        {
            _logger.LogInformation("No guid key present");
        }
        var msg = "Invalid Guid";
        var bytes = Encoding.UTF8.GetBytes(msg);
        httpContext.Response.StatusCode = 403;
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.Body.WriteAsync(bytes, 0, bytes.Length);
        return Task.CompletedTask;
    }
}

Found that solution here:

https://stackoverflow.com/a/61861098/3850405


After opening an issue here, it appears this actually should work...sort of.

In your Startup.Configure, if you just call app.UseMvc() and don't register any other middleware, you will get 401 for any auth-related errors (not authenticated, authenticated but no permission).

If, however, you register one of the authentication middlewares that support it, you will correctly get 401 for unauthenticated and 403 for no permissions. For me, I used the JwtBearerMiddleware which allows authentication via a JSON Web Token. The key part is to set the AutomaticChallenge option when creating the middleware:

in Startup.Configure:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true
});
app.UseMvc();

AutomaticAuthenticate will set the ClaimsPrincipal automatically so you can access User in a controller. AutomaticChallenge allows the auth middleware to modify the response when auth errors happen (in this case setting 401 or 403 appropriately).

If you have your own authentication scheme to implement, you would inherit from AuthenticationMiddleware and AuthenticationHandler similar to how the JWT implementation works.