Custom Unauthorized response body

A quick example to illustrate an implementation of a custom Unauthorized response body in ASP.NET Core 2.0. The implementation is based on the AuthorizeFilter from Microsoft MVC framework.

In the blog post about the implementation of a custom authentication handler I received a comment from Aldo asking how to return a custom Unauthorized response body containing a JSON message in server response when authentication fails. After a quick research I found out that only response code could be changed in the authentication handler, and then decided to write this blog post :)

Authentication and authorization are two different things and should be kept separately. In the context of my simplified Web API endpoints protection it could be put as following:

  • Authentication – check if the key in Authorization header is correct.
  • Authorization – make a decision based on the authentication result. If the authentication failed, return 401 right away, and if succeeded, then let the request through.

To return an error message together with 401 response I had to slightly change authorization behaviour, hence a custom implementation of AuthorizeFilter was needed.

My custom AuthorizeFilter is based on the original Microsoft implementation, however is much simpler, and has following functionality.

  • Allows unauthorized calls to methods and classes decorated with AllowAnonymous attribute.
  • Gets the result of the authentication, and if the authentication failed (was challenged), cancels the request execution and returns JsonResult response with an error message. If authentication result was forbidden, then returns a default ForbidResult. And finally, if the result was successful, does nothing, i.e. lets the request to continue it’s journey down the pipeline.

The implementation code is below.


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

	public CustomAuthorizeFilter(AuthorizationPolicy policy)
	{
		Policy = policy ?? throw new ArgumentNullException(nameof(policy));
	}
	
	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 CustomUnauthorizedResult("Authorization failed.");
		}
		else if (authorizeResult.Forbidden)
		{
			// Return default 403 result
			context.Result = new ForbidResult(Policy.AuthenticationSchemes.ToArray());
		}
	}
}


public class CustomUnauthorizedResult : JsonResult
{
	public CustomUnauthorizedResult(string message)
		: base(new CustomError(message))
	{
		StatusCode = StatusCodes.Status401Unauthorized;
	}
}

public class CustomError
{
	public string Error { get; }

	public CustomError(string message)
	{
		Error = message;
	}
}

To test the custom Unauthorized response body implemented above I’ve combined it with the custom authentication handler implemented previously and made a full working sample Web API application. Source code is on my GitHub. Let’s run several test requests and see how the new authorization filter works.

GET /api/values with a valid authentication key returns 200 OK.

Unauthorized response body - OK

GET /api/values with an invalid or missing authentication key returns 401 Unauthorized with the JSON error message (yay!).

Unauthorized response body - Unauthorized

And just to test AllowAnonymous attribute I’ve added one more API endpoint, see code snippet below.


[AllowAnonymous]
[HttpGet]
[Route("single")]
public string GetSingle()
{
	return "value";
}

And GET /api/values/single with invalid or missing authentication key returns 200 OK, since it’s decorated with AllowAnonymous attribute.

Unauthorized response body - AllowAnonymous

While this custom implementation of AuthorizeFilter does return a JSON error message I wanted to include, however when possible I would use standard Microsoft implementation. This improves maintainability of the application, for example, less breaking changes when upgrading to a new .NET Core version. And if you need an advanced scenario, like using IAuthorizationPolicyProvider, multiple authorization policies etc., the custom Unauthorized response body implementation becomes much more complicated.

 

Source code of the WebAPI and working custom Unauthorized response body is on my GitHub.