Skip to content

Joris Vergeer

Just some software engineer with some skills

Menu
  • Home
Menu

[ASP.net Core] Body based routing with custom MatcherPolicy

Posted on July 9, 2020February 27, 2023 by joris

From time to time you have to implement an API that has to have the same endpoint but has to do something differently based on the content of the request.

For example take the OAuth2 /token endpoint. (This is an example, in real world use you would choose something more secure than this legacy oauth flow)

A post request to that endpoint looks like this:

{
    "grant_type": "password",
    "username": "johndoe523",
    "password": "password123"
}

Then it has to do request validation with something that looks like a username and password.

Sometimes the same endpoint has to handle a request that looks like this:

{
    "grant_type": "refresh_token",
    "refresh_token": "abcd0123efgh4567ijkl8910",
}

In this case it has to do something with a refresh_token.

Of course you can create an single method that handles all cases. But then you might sacrifice on some sweet validation or clean controllers.

With a custom MatcherPolicy and a simple attribute you can match some controller methods against the content of the body.

First the controller to show what we want to archieve:

    [ApiController]
    [Route("api/auth")]
    public class AuthenticationController : ControllerBase
    {
        [Route("token")]
        [GrantType("password")]
        [HttpPost]
        public async Task<IActionResult> PostToken(PasswordGrantTokenRequestDto request)
        {
            // Handle password logic
            return Ok("Response from password grant");
        }

        [Route("token")]
        [GrantType("refresh_token")]
        [HttpPost]
        public async Task<IActionResult> PostToken(RefreshTokenGrantTokenRequestDto request)
        {
            // Handle refresh_token logic
            return Ok("Response from refresh_token grant");
        }

        [Route("token", Order = 10)]
        [HttpPost]
        public async Task<IActionResult> PostToken()
        {
            return Problem(title: "unsupported_grant_type");
        }

        // Dto's are here, normally they are in separate files
        public class PasswordGrantTokenRequestDto
        {
            [Required]
            [JsonPropertyName("username")]
            public string Username { get; set; }

            [Required]
            [JsonPropertyName("password")]
            public string Password { get; set; }
        }

        public class RefreshTokenGrantTokenRequestDto
        {
            [Required]
            [JsonPropertyName("refresh_token")]
            public string RefreshToken { get; set; }
        }
    }

As you can see the first 2 methods do have an GrantType attribute.

The GrantTypeAttribute is simple:

    public class GrantTypeAttribute : Attribute
    {
        public string GrantType { get; private set; }

        public GrantTypeAttribute(string grantType)
        {
            GrantType = grantType;
        }
    }

The real magic happens here, in a custom MatcherPolicy called GrantTypeMatcherPolicy.

    public class GrantTypeMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
    {
        public override int Order { get; } = 100;

        public bool AppliesToEndpoints(IReadOnlyList&lt;Endpoint> endpoints)
        {
            if (endpoints == null)
            {
                throw new ArgumentNullException(nameof(endpoints));
            }

            for (var i = 0; i &lt; endpoints.Count; i++)
            {
                var endpoint = endpoints[i];
                var grantTypeAttribute = endpoint.Metadata.GetMetadata&lt;GrantTypeAttribute>();

                if(grantTypeAttribute != null)
                {
                    return true;
                }
            }

            return false;
        }

        public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
        {
            httpContext.Request.EnableBuffering(30 * 1024, 30 * 1024);

            TokenRequestDto tokenRequestDto = null;

            try
            {
                tokenRequestDto = await JsonSerializer.DeserializeAsync&lt;TokenRequestDto>(httpContext.Request.Body);
            }
            catch {
                // ignore
            }

            httpContext.Request.Body.Position = 0;


            for (var i = 0; i &lt; candidates.Count; i++)
            {
                var candidate = candidates[i];

                var grantTypeAttributes = candidate.Endpoint.Metadata.GetOrderedMetadata&lt;GrantTypeAttribute>();

                if (grantTypeAttributes.Any())
                {
                    if (tokenRequestDto == null)
                    {
                        candidates.SetValidity(i, false);
                    }
                    else
                    {
                        candidates.SetValidity(i, grantTypeAttributes.Any(attr => attr.GrantType == tokenRequestDto.GrantType));
                    }
                }
            }
        }
    }

Here we need to implement the IEndpointSelectorPolicy interface which has 2 methods.

First the AppliesToEndpoints method. This methods returns true when the GrantTypeAttribute is present in one of the endpoints metadata.

Secondly, the ApplyAsync method. This methods first enables buffering. We try to read the body here, and it needs to be read again later. Also note that the position of the body is set back to 0 after the body is read.

First we try to extract an instance of TokenRequestDto to retrieve the grant_type field.

    public class TokenRequestDto
    {
        [JsonPropertyName("grant_type")]
        public string GrantType { get; set; }
    }

Then for each endpoint candidate we check if there are any GrantTypeAttribute present on the endpoint, and if that is the case we set the validity to true if the endpoint if the grant type matches, of false when the grant_type does not match or is not present at all.

The last thing is to register this MatcherPolicy in the ConfigureServices method of the Startup class.

// ...
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, GrantTypeMatcherPolicy>());
// ...

Now the correct method is called depending on a field in the body of the request.

  • asp
  • core
  • dotnet
  • net
  • routing
  • Work

    Currently working for and owner of RetailEntertainment B.V.
    • MKB-Muziek
    • Zorgscherm
    • Zorgstand
    • [Hashicorp Vault/PostgreSQL] Cleanup of roles with permissions and ownership
    • [C++/QT/OpenSSL/JWT] Minimalistic implementation to create a signed JTW token.
    • [C++/QT] QFuture delay method
    • [Vite] Copy vite build output to destination directory
    • [Python][Clang] Extract variabele value from a c++ file in python
    • May 2024 (1)
    • March 2023 (2)
    • February 2023 (1)
    • January 2023 (1)
    • July 2020 (1)
    • November 2019 (1)
    • May 2019 (1)
    • March 2019 (2)
    • DevOps
    • Programming
    • Uncategorized
    • Web

    Meta

    • Log in
    • Entries feed
    • Comments feed
    • WordPress.org
    © 2025 Joris Vergeer | Powered by Minimalist Blog WordPress Theme