asp.net-coreasp.net-authorizationasp.net-authentication

ASP.NET Core : Return Json response on Unauthorized in a filter at the controller/action level


I am not using Identity.

I have this ASP.NET Core configuration enabling two authentication schemes, cookies and basic auth:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "_auth";
        options.Cookie.HttpOnly = true;
        options.LoginPath = new PathString("/Account/Login");
        options.LogoutPath = new PathString("/Account/LogOff");
        options.AccessDeniedPath = new PathString("/Account/Login");
        options.ExpireTimeSpan = TimeSpan.FromHours(4);
        options.SlidingExpiration = true;
    })
    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

BasicAuthenticationHandler is a custom class inheriting from AuthenticationHandler and overriding HandleAuthenticateAsync to check the request headers for basic authentication challenge, and returns either AuthenticateResult.Fail() or AuthenticateResult.Success() with a ticket and the user claims.

It works fine as is:

My goal is to have most of my project to use Cookies (hence why it is set as default), but have some API type of controllers to accept both methods. It should also be possible to tag the Controllers/Actions to return a specific Json body when desired (as opposed to the login redirect or base 401 response), but only for certain controllers.

I've spent the last 2 days reading different similar questions and answers here on StackOverflow, nothing seems to accommodate my need.

Here's a few methods I found:

I'm thinking either there's a filter type I'm missing, or maybe I need to create a third authentication type that will mix the previous two and control the response from there. Finding a way to add a custom error message in the options would also be nice.


Solution

  • I managed to do it via a IAuthorizationMiddlewareResultHandler. Not my favorite approach because there can be only one per project and it intercepts all calls, but by checking if a specific (empty) attribute is set, I can control the flow:

    public class JsonAuthorizationAttribute : Attribute
    {
        public string Message { get; set; }
    }
    
    public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
    {
        private readonly AuthorizationMiddlewareResultHandler DefaultHandler = new AuthorizationMiddlewareResultHandler();
    
        public async Task HandleAsync(RequestDelegate requestDelegate, HttpContext httpContext, AuthorizationPolicy authorizationPolicy, PolicyAuthorizationResult policyAuthorizationResult)
        {
            // if the authorization was forbidden and the resource had specific attribute, respond as json
            if (policyAuthorizationResult.Forbidden)
            {
                var endpoint = httpContext.GetEndpoint();
                var jsonHeader = endpoint?.Metadata?.GetMetadata<JsonAuthorizationAttribute>();
                if (jsonHeader != null)
                {
                    var message = "Invalid User Credentials";
    
                    if (!string.IsNullOrEmpty(jsonHeader.Message))
                        message = jsonHeader.Message;
    
                    httpContext.Response.StatusCode = 401;
                    httpContext.Response.ContentType = "application/json";
                    var jsonResponse = JsonSerializer.Serialize(new
                    {
                        error = message
                    });
    
                    await httpContext.Response.WriteAsync(jsonResponse);
                    return;
                }
            }
    
            // Fallback to the default implementation.
            await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
        }
    }