How to send custom 401 Response in ASP.NET Core

In this detailed article, let's find out how we can send custom Unauthorized responses to users and explore 3 ways to do it

Introduction

The JwtBearer Authentication middleware which we can configure and use for validating incoming request tokens and authorize access to requests responds to Unauthorized requests with a plain 401 status code without any response body. Sometimes we might come across situations like in a unified response design or others, where we might need to send meaningful response body to the clients in case of Unauthorized scenarios.

In this article, let’s explore some possible ways to customize the response body when the Authorization middleware decides to send an Unauthorized response to the user.

How to send custom Unauthorized (401) Responses in ASP.NET Core Web API

We shall look at 3 possible solutions in this case, we shall also try to understand each of these solutions and how they might be a fit for us.

To setup our case, let’s create a simple AspNetCore webapi project. We shall not touch any of the boilerplate controller WeatherForecastController, but instead try to add authorized access to the default Action endpoint present in there and see how it can be tweaked for Unauthorized cases.

> dotnet new webapi --name uauthapi

To add an authentication layer to it, first we need an OAuth Server that can produce JWT Tokens for our case. To do this, I’d reuse my ids4.simple project based on the IdentityServer4 framework that we used to create simple Clients. This one uses a client_credentials grant type just to keep things simple.

Once we have the project (unauthapi) created and ready, we’d now install the JwtBearer library to be used for authentication / authorization.

> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Once the library is installed, let’s add the JwtBearer service to the Authentication middleware in the services and the pipeline.

// Startup.ConfigureServices method

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer((options) =>
{
    options.Authority = "https://localhost:5002";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateLifetime = true,
        ValidateAudience = false
    };
});

// Startup.Configure method

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints((options) => {
    options.MapControllersWithDefaultRoute();
});

The Authority refers to the baseUrl of the OAuth Server that runs parallel to this WebAPI project. We’ve configured to validate the lifetime of the incoming token along with its signature (default) but need not validate the audience. We’re now set with our configurations.

Finally we’d add Authorize attribute on top of the WeatherForecastController.

[Authorize]
[Route("[controller]")]
[ApiController]
public class WeatherForecastController : ControllerBase 
{
    // actions and code
    public IEnumerable<string> Get() {
        return new List<string>() {"A", "B", "C"};
    }
}

Now when we hit this on a browser (since it’s a GET call) we’d receive a 401 response code with no response content.

Solution 1 – Using OnChallenge Event Handler

While adding the JwtBearer service to the Authentication service, we can also override the default Events that are triggered during various phases of the validation and create our own customization on top of it.

JwtBearer provides 4 JwtBearerEvents by default:

  1. OnMessageReceived – when a new request is received to the API, this is triggered. Here we can examine the request context and the token.
  2. OnTokenValidated – triggered when the token is successfully validated and the request is forwarded to the intended action
  3. OnAuthenticationFailed – triggered when the token fails validation
  4. OnChallenge – triggered when the response is about to be pushed out of the server to the client

Among these four events, the OnChallenge event happens the last and it is where the response context is set before sending out to the client. Hence we can use this place to add our custom message.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer((options) =>
{
    options.Authority = "https://localhost:5002";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateLifetime = true,
        ValidateAudience = false
    };
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = async (context) =>
        {
            Console.WriteLine("Printing in the delegate OnAuthFailed");
        },
        OnChallenge = async (context) =>
        {
            Console.WriteLine("Printing in the delegate OnChallenge");

            // this is a default method
            // the response statusCode and headers are set here
            context.HandleResponse();

            // AuthenticateFailure property contains 
            // the details about why the authentication has failed
            if (context.AuthenticateFailure != null)
            {
                context.Response.StatusCode = 401;

                // we can write our own custom response content here
                await context.HttpContext.Response.WriteAsync("Token Validation Has Failed. Request Access Denied");
            }
        }
    };
});

Solution 2 – Using a Custom Authorize Filter

This approach is quite straight forward. It relies on overriding the default behavior of an Authorize attribute by implementing the IAsyncAuthorizationFilter (the async version of the IAuthorizationFilter) and customizing the response by setting a custom Result onto the context. Since we’re working with controllers, this approach works quite great.

In this approach, we implement the OnAuthorizationAsync() method and here we try to add the authorization logic by ourselves. What we basically do here is:

  1. Request an instance of a PolicyEvaluator from the ServiceProvider
  2. Authenticate the current request context based on the Policy
  3. Authorize the AuthenticateResult received from the previous step and decide if the request context can be authorized or denied

if the AuthorizeResult (from the step 3) is set to “Challenged” (similar to the callback we were using in Solution 1) we can set a JsonResult to the context, with a StatusCode (401 in this case) and an object that can be any structure we desire. In this case I’m passing a single Message property in the object with some text.

public class CustomAuthorizationFilter : IAsyncAuthorizationFilter
{
    public AuthorizationPolicy Policy { get; }

    public CustomAuthorizationFilter()
    {
        Policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        // Allow Anonymous skips all authorization
        if (context.Filters.Any(item => item is IAllowAnonymousFilter))
        {
            return;
        }

        var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();
        var authenticateResult = await policyEvaluator.AuthenticateAsync(Policy, context.HttpContext);
        var authorizeResult = await policyEvaluator.AuthorizeAsync(Policy, authenticateResult, context.HttpContext, context);

        if (authorizeResult.Challenged)
        {
            // Return custom 401 result
            context.Result = new JsonResult(new
            {
                Message = "Token Validation Has Failed. Request Access Denied"
            })
            {
                StatusCode = StatusCodes.Status401Unauthorized
            };
        }
    }
}

We’ll now have our JwtBearer service added, but without any events (like in Solution 1) so that the general authentication is set. This is used when the CustomAuthorizationFilter is executed.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer((options) =>
{
    options.Authority = "https://localhost:5002";
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateLifetime = true,
        ValidateAudience = false
    };
});

We’ll now decorate our WeatherForecastController with the custom filter we’ve created as below:

[TypeFilter(typeof(CustomAuthorizationFilter))]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    // all that code
}

If we’re not interested in calling our Filter as a TypeFilter, we can set it up to be used as a decorator by extending the ActionFilterAttribute.

public class CustomAuthorizationFilter : ActionFilterAttribute, IAsyncAuthorizationFilter
{
    // validation code
}

[ApiController]
[Route("[controller]")]
[CustomAuthorizationFilter]
public class WeatherForecastController : ControllerBase
{
    // action methods
}

Solution 3 – Use a Middleware to write to the Response Stream

The simplest and my personal favorite is – to just use a custom middleware. By design, a middleware when added to the pipeline works in the LIFO fashion, meaning the response after being processed must pass through the middleware that it passes before execution. So we can simply design a custom middleware by using app.Use() that captures the response before leaving the server and writes a custom response body to the stream. To wrap this up, we’d add a condition on the ResponseStatusCode so that this is executed only when the StatusCode is UnAuthorized.

// Startup.Configure method

app.UseRouting();

app.Use(async (context, next) =>
{
    await next();

    if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
    {
        await context.Response.WriteAsync("Token Validation Has Failed. Request Access Denied");
    }
});

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints();

Buy Me A Coffee

Found this article helpful? Please consider supporting!

Conclusion

Writing custom responses the case of an UnAuthorized request can be quite tricky, particularly when we’re relying on the built-in authentication and authorization middleware provided by the AspNetCore framework. But we do have a few shortcuts/workarounds/hacks using which we can get our expected results, some of which we discussed above.

  • The first approach (OnChallenge event handling) and the third approach (Custom middleware) are my personal favorites since they’re simple and easy to use.
  • But if we need to add more complex customizations to our response objects, say like adding a custom response code or more properties to the response body, we can go the longer path – using a custom filter.

Which one do you think are helpful to you? Do let me know!


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *