Andrew Williamson
Andrew Williamson

Reputation: 8696

Injecting route values based on the current Principal

I've got a web api that accepts authentication through either an api key, a user access token, or a client access token. I've already written a DelegatingHandler for each case, that creates a new ClaimsPrincipal based on the given authentication details, and have confirmed that the principal is accessible within a controller action.

What I want to do now is inject either a company, a user, or a publisher into the route values, so I can create overloads on the controller for each case. What class/interface do I need to extend in order to plug into the pipeline with the following conditions:

Edit

I'm not looking to sidestep routing here - I still want MVC to choose a route for me, based on the route values. I just want to add one more parameter to the route values before it chooses, and the parameter will have a different name and type depending on whether a user access token is used, or an api key is used.

I believe the subject of authentication should result in two distinct methods, because an api key can access all resources for a company, while a user access token can only access the resources they have been given permission to view.

Upvotes: 1

Views: 681

Answers (2)

Andrew Williamson
Andrew Williamson

Reputation: 8696

I managed to get this going by making a custom ValueProviderFactory, which reads values from the current principal's claims and makes them available for parameter binding:

public class ClaimsPrincipalValueProviderFactory : ValueProviderFactory
{
    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        if (actionContext.RequestContext.Principal != null && actionContext.RequestContext.Principal is ClaimsPrincipal principal)
        {
            var pairs = principal.Claims.Select(claim => new KeyValuePair<string, string>(claim.Type, claim.Value));
            return new NameValuePairsValueProvider(pairs, CultureInfo.CurrentCulture);
        }

        return null;
    }
}

In order to use it, you can annotate the input parameters with the ValueProvider attribute:

public class FooController : ApiController
{
    [HttpGet]
    public void Bar([ValueProvider(typeof(ClaimsPrincipalValueProviderFactory))]ApiKey apiKey)
    {
        // ...
    }
}

That's pretty ugly and unreadable, what I really wanted was something like the FromUri attribute, but for claims. ValueProviderAttribute and FromUriAttribute both inherit from ModelBinderAttribute, so I created a class which does the same:

/// <summary>
/// The action parameter comes from the user's claims, if the user is authorized.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class FromClaimsAttribute : ModelBinderAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        return parameter.BindWithModelBinding(new ClaimsPrincipalValueProviderFactory());
    }
}

Now the example method on the FooController looks a lot more readable:

[HttpGet]
public void Bar([FromClaims]ApiKey apiKey)
{
    // ...
}

Update

Looks like this is still having problems with route selection and overloading, especially when some of the parameters are nullable and the value is null. I'm going to have to keep looking into this.

Update #2

I managed to simplify the value provider stuff a lot, after finding there is a built-in NameValuePairValueProvider

Upvotes: 1

Technetium
Technetium

Reputation: 6158

I do not see a reason why you would want to go with a controller here. You would be sidestepping routing, a very opinionated piece of MVC. I would create middleware that runs before MVC (which is, itself, just middleware) instead.


If you're looking to affect RouteData inline, I would look into using a global IResourceFilter or IAsyncResourceFilter. Then, you can update the RouteData property on the ResourceExecutingContext based upon the conditions you specified in your question.

Any additional dependencies you need to determine how to populate the RouteData property can be injected into the resource filter's constructor as specified in the section on dependency injection.

public class SetRouteValueResourceFilter : IAsyncResourceFilter {

    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) {
        var company = context.HttpContext.Identity.FindFirst("Company")?.Value;

        if (company != null) {
            context.RouteData.Values.Add("company", company);
        }

        await next();
    }

}

This answer is just an idea. Even though this is handled before the action logic, I'm not sure it will affect which route is selected. According to this diagram, routes are selected before filters are ran.

Upvotes: 1

Related Questions