Dávid Kaya

Share this post

Custom From* attributes for controller action methods in ASP.NET Core

www.davidkaya.com

Custom From* attributes for controller action methods in ASP.NET Core

Implementing custom From* attributes for controller action methods in ASP.NET Core.

Dávid Kaya
Jan 5, 2021
Share
Custom From* attributes for controller action methods in ASP.NET Core

Recently I was going through some controllers and noticed a lot of code that was reading claims from ClaimsPrinciple. If you ever worked with claims in ASP.NET Core then code like User.FindFirst(...) might be familiar. All that the method does is that it will find the first claim that matches the type you want. You might have also noticed that, if you wanted to unit test some method in the controller that uses claims, it actually requires a quite a few extra lines of code to achieve that. You have to create ClaimsPrincipal, ClaimsIdentity and Claims and then set everything on the HttpContext of your ControllerContext.

I stopped for a second and said to myself: "It would be nice if I could just get the required claim as a method parameter". And then I realized that it might not be that hard to actually achieve that! Let's see what we have to do to implement a custom [FromClaim] attribute.

Representing our source of data for binding

We will start by creating a BindingSource for our source of data. This is needed when we want to bind our claim to the parameter of our controller action method.

public static class ClaimBindingSource
{
    public static readonly BindingSource Claim = new(
        "Claim", // ID of our BindingSource, must be unique
        "BindingSource_Claim", // Display name
        isGreedy: false, // Marks whether the source is greedy or not
        isFromRequest: true); // Marks if the source is from HTTP Request
}

We will use the BindingSource that we have created in the following steps.

Creating the value provider

The next very important step is to create the value provider. The value provider is the class where the actual claims will be read from the ClaimsPricinpal. We will base our new ClaimValueProvider on the BindingSourceValueProvider since we want to bind  our claims to parameters in our controller action methods.

public class ClaimValueProvider : BindingSourceValueProvider
{
    private readonly ClaimsPrincipal _claimsPrincipal;

    public ClaimValueProvider(BindingSource bindingSource, ClaimsPrincipal claimsPrincipal) : base(bindingSource)
    {
        _claimsPrincipal = claimsPrincipal;
    }

    public override bool ContainsPrefix(string prefix) 
        => _claimsPrincipal.HasClaim(claim => claim.Type == prefix);

    public override ValueProviderResult GetValue(string key)
    {
        var claimValue = _claimsPrincipal.FindFirstValue(key);
        return claimValue != null ? new ValueProviderResult(claimValue) : ValueProviderResult.None;
    } 
}

In our ClaimValueProvider we will need a BindingSource which will be the one we created for claims and a ClaimsPrincipal from which we will read the required claim.

In the ContainsPrefix(...) method, we will check whether the required claim is available.

In the GetValue(...) method, we will return the first claim that matches the required claim type.

This is all we need to do in the value provider. As you can see, you can easily unit test this class, since it does only a couple of small things.

Creating the value provider factory

Now when we have implemented the ClaimValueProvider, we will need something that will create it with correct BindingSource and ClaimsPrincipal. This will be done by implementing a new IValueProviderFactory which we will call ClaimValueProviderFactory.

public class ClaimValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        context.ValueProviders.Add(new ClaimValueProvider(ClaimBindingSource.Claim, context.ActionContext.HttpContext.User));
        return Task.CompletedTask;
    }
}

We will pass our claim-specific BindingSource and also pass the ClaimsPrincipal from the HttpContext that is available in the ValueProviderFactoryContext which we get.

Once you have your provider created, you have to also add it into the available value providers in the ValueProviderFactoryContext.

The last step is to hook up our ClaimValueProviderFactory into available value provider factories which ASP.NET Core uses. You can do that in the AddControllers[WithViews](...) method in ConfigureServices(...).

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddControllersWithViews(options => options.ValueProviderFactories.Add(new ClaimValueProviderFactory()));
    // ...
}

The last but not the least, our custom [FromClaim] attribute

So far we have implemented all the logic that will extract the claim from the ClaimsPrincipal and hooked up everything in ASP.NET Core. What is missing is our [FromClaim] attribute itself.

Our attribute will implement two interfaces:

  1. IBindingSourceMetadata  - this will specify which binding source will be used for binding

  2. IModelNameProvider - this will specify the model name, this is the key that we will get in the value provider as a parameter

We will also add a constructor which receives a claim type, so we have to specify the claim type when we use it in our controller action method, e.g. [FromClaim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")] .

[AttributeUsage(AttributeTargets.Parameter)]
public class FromClaimAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
    public BindingSource BindingSource => ClaimBindingSource.Claim;

    public FromClaimAttribute(string type)
    {
        Name = type;
    }

    public string Name { get; }
}

Using our new attribute in controller action method

Now when we have everything implemented, we can easily use our new attribute in the controller action method.

public IActionResult Index([FromClaim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")] string name /* This will have the value of the claim */)
{
    return View();
}
Share
Comments
Top
New
Community

No posts

Ready for more?

© 2023 Dávid Kaya
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing