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
- 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; }
}
- 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;
}
- 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();
}
- 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));
}
}
- 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));
}
}
- 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)
{
...
}