Using HTTP Request Routes, Request Body, and Query string parameters for Authorization in ASP.NET Core

This post shows how HTTP route parameters, a HTTP request body or HTTP request query string parameters can be used for authorization in ASP.NET Core.

Code: https://github.com/damienbod/AspNetCoreWindowsAuth

Authorization using ASP.NET Core Route parameters

An AuthorizationHandler can be used to implement authorization logic in ASP.NET Core. The handler can authorize HTTP requests using a route parameter from where the policy for the requirement used in the handler is defined. The IHttpContextAccessor is used to access the route parameters. The RouteValues property in the request of the HttpContext contains these values. If you know the name of the route value, the value can be retrieved using this key. In this demo, a static text is used to validate the route parameter value. In a real world AuthorizationHandler, the value would be validated against a claim from the token, or queried from a database, or an authorization service. To validate this correctly, something must be used which cannot be manipulated. If using a claim from the access token, then the access token must be validated fully and correctly. The AuthorizationHandler implements the ValuesRouteRequirement which is used in the policy definition.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckRouteParameterHandler : AuthorizationHandler<ValuesRouteRequirement>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

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

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValuesRouteRequirement requirement)
        {
            var routeValues = _httpContextAccessor.HttpContext.Request.RouteValues;

            object user;
            routeValues.TryGetValue("user", out user);
            if ( user.ToString() == "phil")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

In the Startup class using the ConfigureServices method, the IAuthorizationHandler services are registered and also the IHttpContextAccessor using the AddHttpContextAccessor method. The policies are defined for the authorization requirements. The demo is an API project example, which uses swagger so the AddControllers extension method is used, with AddNewtonsoftJson.

public void ConfigureServices(IServiceCollection services)
{
	// ...

	services.AddHttpContextAccessor();

	services.AddSingleton<IAuthorizationHandler, ValuesCheckQueryParameterHandler>();
	services.AddSingleton<IAuthorizationHandler, ValuesCheckRequestBodyHandler>();
	services.AddSingleton<IAuthorizationHandler, ValuesCheckRouteParameterHandler>();

	services.AddAuthorization(options =>
	{
		options.AddPolicy("protectedScope", policy =>
		{
			policy.RequireClaim("scope", "native_api");
		});
		options.AddPolicy("ValuesRoutePolicy", valuesRoutePolicy =>
		{
			valuesRoutePolicy.Requirements.Add(new ValuesRouteRequirement());
		});
		options.AddPolicy("ValuesQueryPolicy", valuesQueryPolicy =>
		{
			valuesQueryPolicy.Requirements.Add(new ValuesCheckQueryParamRequirement());
		});
		options.AddPolicy("ValuesRequestBodyCheckPolicy", valuesRequestBodyCheckPolicy =>
		{
			valuesRequestBodyCheckPolicy.Requirements.Add(new ValuesRequestBodyRequirement());
		});
	});


	services.AddControllers()
		.AddNewtonsoftJson();
}

The policy is then used in the controller in the authorize attribute. In this demo, if the user has the value ‘phil’, the data will be returned, otherwise a 403 is returned, or 401 if no bearer access token is sent in the header of the HTTP request.

[Authorize("ValuesRoutePolicy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet]
[Route("{user}", Name = nameof(GetWithRouteParam))]
public IActionResult GetWithRouteParam([FromRoute]string user)
{
	return Ok($"get this data [{user}] using the route");
}

A HttpClient implementation can then make a HTTP request with the route set and the access token added to the headers.

private static async Task CallApiwithRouteValue(string currentAccessToken, string user)
{
	_apiClient.SetBearerToken(currentAccessToken);
	var response = await _apiClient.GetAsync($"/api/values/{user}");

	if (response.IsSuccessStatusCode)
	{
		var result = await response.Content.ReadAsStringAsync();
		Console.WriteLine($"\n{result}");
	}
	else
	{
		Console.WriteLine($"Error: {response.ReasonPhrase}");
	}
}

Authorization using HTTP Query string parameters

ASP.NET Core query parameters can also be used inside an AuthorizationHandler in almost the same way as the route parameter. The IHttpContextAccessor is used to get the HttpContext. Then the Query request property can be used to access the parameters. In this demo, the query parameter is named fruit which can be used to retrieve the value. If it equals oranges, the requirement will succeed using this handler.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckQueryParameterHandler : AuthorizationHandler<ValuesCheckQueryParamRequirement>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

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

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValuesCheckQueryParamRequirement requirement)
        {
            var queryString = _httpContextAccessor.HttpContext.Request.Query;
            var fruit = queryString["fruit"];

            if (fruit.ToString() == "orange")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

The controller class can then be authorized with the Authorize attribute using the ValuesQueryPolicy which checks for the requirement, which was used in the ValuesCheckQueryParameterHandler.

[Authorize("ValuesQueryPolicy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet]
[Route("q/{user}", Name = nameof(GetWithQueryParam))]
public IActionResult GetWithQueryParam([FromRoute]string user, [FromQuery]string fruit)
{
	return Ok($"get this data [{fruit}] using the query parameter");
}

A HttpClient can then be used to send the HTTP request with the query string parameter.

private static async Task CallApiwithQueryStringParam(string currentAccessToken, string fruit)
{
	_apiClient.SetBearerToken(currentAccessToken);
	var response = await _apiClient.GetAsync($"/api/values/q?fruit={fruit}");

	if (response.IsSuccessStatusCode)
	{
		var result = await response.Content.ReadAsStringAsync();
		Console.WriteLine( $"\n{result}");
	}
	else
	{
		Console.WriteLine($"Error: {response.ReasonPhrase}");
	}
}

Authorization using the HTTP Request Body

The body of a HTTP POST request can also be used to do authorization. This works slightly different to the previous two examples. The ValuesCheckRequestBodyHandler implements the AuthorizationHandler with the ValuesRequestBodyRequirement requirement and also the BodyData resource. This resource is used to do the authorization checks as required.

using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckRequestBodyHandler : AuthorizationHandler<ValuesRequestBodyRequirement, BodyData>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
              ValuesRequestBodyRequirement requirement, BodyData bodyData)
        {
            if(bodyData.User == "mike")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }

    public class BodyData
    {
        public string User { get; set; }
    }
}

In the Controller, the Authorize attribute is not used. This is because we do not want to deserialize the body a second time. Once inside the controller method, the body data, which has already been serialized is passed as a parameter to the authorization check. This has the disadvantage that the authorization is executed later in the pipeline. The authorization is then used in the method and a Forbidden 403 is returned, if the body data sent has unauthorized values.

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(typeof(BodyData))]
[HttpPost]
[Route("", Name = nameof(Post))]
public async Task<IActionResult> Post([FromBody]BodyData user)
{
	var requirement = new ValuesRequestBodyRequirement();
	var resource = user;

	var authorizationResult =
		await _authorizationService.AuthorizeAsync(
			User, resource, requirement);

	if (authorizationResult.Succeeded)
	{
		return Ok($"posted this data [{user.User}] using the body");
	}
	else
	{
		return new ForbidResult();
	}
}

It is really easy to use the different parts of the HTTP request, to do the specific authorization as required.

Links:

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies

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

https://docs.microsoft.com/en-us/aspnet/core/razor-pages

2 comments

  1. […] Using HTTP Request Routes, Request Body, and Query string parameters for Authorization in ASP.NET Co… (Damien Bowden) […]

  2. […] Using HTTP Request Routes, Request Body, and Query string parameters for Authorization in ASP.NET Co… – Damien Bowden […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.