Home > other >  Azure Function Custom Input Binding -- Pass both Input from Attribute and HttpRequest to IValueProvi
Azure Function Custom Input Binding -- Pass both Input from Attribute and HttpRequest to IValueProvi

Time:08-05

I am trying to build a custom input binding that takes an array of strings (which are actually scopes) and validate that the scopes in the array are available in the JWT of the HttpRequest (as demonstrated below).

[FunctionName(nameof(Put))]
    public async Task<IActionResult> Put(
        [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "route")] HttpRequest req,
        ILogger log,
        [AuthorizeFunction(new[] { Scopes.Delete })] AuthorizationResult result
    )

Currently, I have an HttpTrigger with an associated HttpRequest that contains the contents of the Authorization Header. The issue I'm encountering is during the creation of the binding rule in my implementation of the IExtensionConfigProvider.

public class AuthorizationExtensionProvider : IExtensionConfigProvider
{
    public AuthorizationExtensionProvider() { }
    public void Initialize(ExtensionConfigContext context)
    {
        // Creates a rule that links the attribute to the binding
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        var rule = context.AddBindingRule<AuthorizeFunctionAttribute>();
        rule.BindToInput(BuildInputFromAttribute);
    }

    public Task<string[]> BuildInputFromAttribute(
        AuthorizeFunctionAttribute arg,
        ValueBindingContext context
    )
    {
        return Task.FromResult(arg.Scopes);
    }
}

In the example above, I am able to retrieve the input from the Binding, but am unable to pass that input to a IBindingProvider where I can simultaneously extract the contents of the HttpRequest.

Alternatively, in the code sample below, I am able to create a rule that binds the Attribute to an IBindingProvider which allows me to pass the context (including the HttpRequest) to an IValueProvider.

public void Initialize(ExtensionConfigContext context)
    {
        // Creates a rule that links the attribute to the binding
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        var rule = context.AddBindingRule<AuthorizeFunctionAttribute>();
        rule.Bind(new AuthorizationBindingProvider());
    }

All of this of course being a longwinded way of asking how to both retrieve the contents of an input binding's arguments while also providing them to a IValueProvider. Any help would be greatly appreciated.

CodePudding user response:

I found a good article on how to create custom binding for azure function: Custom Binding for Azure Functions

Here is a solution I've came out with:

  • Azure function v4 / dotnet
  • Nuget packages: Microsoft.NET.Sdk.Functions 4.1.1, Microsoft.Azure.Functions.Extensions 1.1.0, Microsoft.AspNetCore.Authentication.JwtBearer 6.0.7
  1. So first I have an attribute that will be use to define the authorized scopes:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
[Binding]
public  sealed class AuthorizeFunctionAttribute : Attribute
{
    public AuthorizeFunctionAttribute(string[] scopes)
    {
        Scopes = scopes;
    }

    public string[] Scopes { get; set; }    
}
  1. The IValueProvider implementation will be responsible for checking the attribute scopes in the request:
public class AuthorizeFunctionValueProvider : IValueProvider
{
    private readonly AuthorizeFunctionAttribute _attribute;
    private readonly HttpRequest _request;        
    private readonly ILogger _logger;        

    public AuthorizeFunctionValueProvider(AuthorizeFunctionAttribute attribute, HttpRequest request, ILogger logger)
    {
        _attribute = attribute;
        _request = request;            
        _logger = logger;
    }

    public Task<object> GetValueAsync()
    {
        try
        {
            // Here we assume that the token has already been validated
            var tokenHeaderValue = _request.Headers["Authorization"].ToString();
            var jwtString = tokenHeaderValue.Replace("Bearer ", "");
            var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(jwtString);

            // filter on the type of claims you would like to check
            if(jwtToken.Claims.Any(c => c.Type == "roles" && _attribute.Scopes.Contains(c.Value)))
                return Task.FromResult((object)AuthorizationResult.Success());
            else
                return Task.FromResult((object)AuthorizationResult.Failed());
        }
        catch (Exception ex)
        {
            _logger.LogCritical(ex, "Error reading authorization header");
            throw;
        }
    }

    public Type Type => typeof(AuthorizationResult);

    public string ToInvokeString() => string.Empty;
}
  1. The IBinding implementation will be responsible for injecting the http request:
public class AuthorizeFunctionBinding : IBinding
{
    private readonly AuthorizeFunctionAttribute _attribute;
    private readonly ILogger _logger;

    public AuthorizeFunctionBinding(AuthorizeFunctionAttribute attribute, ILogger logger)
    {
        _attribute = attribute;
        _logger = logger;
    }

    public Task<IValueProvider> BindAsync(BindingContext context)
    {
        // Get the HTTP request
        var request = context.BindingData["req"] as HttpRequest;
        return Task.FromResult<IValueProvider>(new AuthorizeFunctionValueProvider(_attribute, request, _logger));
    }

    public bool FromAttribute => true;

    public Task<IValueProvider> BindAsync(object value, ValueBindingContext context)
    {
        return null;
    }

    public ParameterDescriptor ToParameterDescriptor() => new ParameterDescriptor();
}
  1. The IBindingProvider implementation will be responsible for getting the attribute scopes:
public class AuthorizeFunctionBindingProvider : IBindingProvider
{
    private readonly ILogger _logger;

    public AuthorizeFunctionBindingProvider(ILogger logger)
    {
        _logger = logger;
    }

    public Task<IBinding> TryCreateAsync(BindingProviderContext context)
    {
        var attr = context.Parameter.GetCustomAttribute<AuthorizeFunctionAttribute>(inherit: false);
        return Task.FromResult((IBinding)new AuthorizeFunctionBinding(attr, _logger));
    }
}
  1. The IExtensionConfigProvider implementation will be responsible for adding a binding rule to the azure function configuration:
public class BindingExtensionProvider : IExtensionConfigProvider
{
    private readonly ILogger _logger;

    public BindingExtensionProvider(ILogger<Startup> logger)
    {
        _logger = logger;
    }

    public void Initialize(ExtensionConfigContext context)
    {
        context.AddBindingRule<AuthorizeFunctionAttribute>().Bind(new AuthorizeFunctionBindingProvider(_logger));
    }
}
  1. The Startup class will register the dependencies:
[assembly: FunctionsStartup(typeof(FunctionApp2.Startup))]
namespace FunctionApp2
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // IWebJobsBuilders instance
            var wbBuilder = builder.Services.AddWebJobs(x => { return; });

            // And now you can use AddExtension
            wbBuilder.AddExtension<BindingExtensionProvider>();
        }
    }
}

you can now use the new binding in your function:

[FunctionName("Function1")]
public static async Task<IActionResult> Run1(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log,
    [AuthorizeFunction(new[] { "Delete" })] AuthorizationResult result)
{
    ...
}

[FunctionName("Function2")]
public static async Task<IActionResult> Run2(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log,
    [AuthorizeFunction(new[] { "Create", "update" })] AuthorizationResult result)
{
    ...
}
  • Related