diff --git a/.build/release.props b/.build/release.props index 21447f8..b3888d9 100644 --- a/.build/release.props +++ b/.build/release.props @@ -6,9 +6,9 @@ DarkLoop DarkLoop - All rights reserved DarkLoop's Azure Functions Authorization - false + true 4.0.0.0 - 4.1.1 + 4.1.2 $(Version).0 https://github.com/dark-loop/functions-authorize https://github.com/dark-loop/functions-authorize/blob/master/LICENSE diff --git a/sample/SampleIsolatedFunctions.V4/Program.cs b/sample/SampleIsolatedFunctions.V4/Program.cs index 03bb91e..43db859 100644 --- a/sample/SampleIsolatedFunctions.V4/Program.cs +++ b/sample/SampleIsolatedFunctions.V4/Program.cs @@ -2,10 +2,8 @@ // Copyright (c) DarkLoop. All rights reserved. // -using System.IdentityModel.Tokens.Jwt; using Common.Tests; using DarkLoop.Azure.Functions.Authorization; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj b/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj index 5627229..050e5ad 100644 --- a/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj +++ b/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj @@ -9,10 +9,6 @@ - - - - diff --git a/sample/SampleIsolatedFunctions.V4/TestFunction.cs b/sample/SampleIsolatedFunctions.V4/TestFunction.cs index 3cfeed0..b92d29c 100644 --- a/sample/SampleIsolatedFunctions.V4/TestFunction.cs +++ b/sample/SampleIsolatedFunctions.V4/TestFunction.cs @@ -14,7 +14,7 @@ namespace SampleIsolatedFunctions.V4 { - [FunctionAuthorize(AuthenticationSchemes = "Bearer")] + [FunctionAuthorize(AuthenticationSchemes = "FunctionsBearer")] public class TestFunction { private readonly ILogger _logger; diff --git a/src/abstractions/FunctionsAuthorizationProvider.cs b/src/abstractions/FunctionsAuthorizationProvider.cs index da66f2f..8881566 100644 --- a/src/abstractions/FunctionsAuthorizationProvider.cs +++ b/src/abstractions/FunctionsAuthorizationProvider.cs @@ -70,8 +70,8 @@ public async Task GetAuthorizationAsync(string func return filter!; } - var asyncKey = $"fap:{functionName}"; - + // ensuring key is interned before entering monitor since key is compared as object + var asyncKey = string.Intern($"fap:{functionName}"); await KeyedMonitor.EnterAsync(asyncKey, unblockOnFirstExit: true); try diff --git a/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj b/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj index 85b85f9..621dac5 100644 --- a/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj +++ b/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj @@ -21,9 +21,8 @@ - - + diff --git a/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs b/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs index 45e179b..a2054fb 100644 --- a/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs +++ b/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using DarkLoop.Azure.Functions.Authorization.Extensions; using DarkLoop.Azure.Functions.Authorization.Features; +using DarkLoop.Azure.Functions.Authorization.Internal; using Microsoft.AspNetCore.Authorization; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Middleware; @@ -39,9 +40,9 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next return; } - if(!_options.IsFunctionRegistered(context.FunctionDefinition.Name)) + if (!_options.IsFunctionRegistered(context.FunctionDefinition.Name)) { - RegisterHttpTriggerAuthorization(context); + await RegisterHttpTriggerAuthorizationAsync(context); } context.Features.Set( @@ -50,27 +51,45 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next await next(context); } - private void RegisterHttpTriggerAuthorization(FunctionContext context) + private async Task RegisterHttpTriggerAuthorizationAsync(FunctionContext context) { - var functionName = context.FunctionDefinition.Name; - var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch + // Middleware can be hit concurrently, we need to ensure this functionality + // is thread-safe on a per function basis. + // Ensuring key is interned before entering monitor since key is compared as object + var monitorKey = string.Intern($"famm:{context.FunctionId}"); + await KeyedMonitor.EnterAsync(monitorKey); + + try { - -1 => string.Empty, - var index => context.FunctionDefinition.EntryPoint[..index] - }; + if (_options.IsFunctionRegistered(context.FunctionDefinition.Name)) + { + return; + } + + var functionName = context.FunctionDefinition.Name; + var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch + { + -1 => string.Empty, + var index => context.FunctionDefinition.EntryPoint[..index] + }; - var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..]; - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false)) - .FirstOrDefault(t => t is not null)? - .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ?? - throw new MethodAccessException( - $"Method instance for function '{context.FunctionDefinition.Name}' " + - $"cannot be found or cannot be accessed due to its protection level."); + var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..]; + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false)) + .FirstOrDefault(t => t is not null)? + .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ?? + throw new MethodAccessException( + $"Method instance for function '{context.FunctionDefinition.Name}' " + + $"cannot be found or cannot be accessed due to its protection level."); - var declaringType = method.DeclaringType!; + var declaringType = method.DeclaringType!; - _options.RegisterFunctionAuthorizationAttributesMetadata(functionName, declaringType, method); + _options.RegisterFunctionAuthorizationAttributesMetadata(functionName, declaringType, method); + } + finally + { + KeyedMonitor.Exit(monitorKey); + } } } } diff --git a/test/Isolated.Tests/ConcurrentTests.cs b/test/Isolated.Tests/ConcurrentTests.cs new file mode 100644 index 0000000..b5009de --- /dev/null +++ b/test/Isolated.Tests/ConcurrentTests.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) DarkLoop. All rights reserved. +// + +using System.Net; + +namespace Isolated.Tests +{ + [TestClass] + public class ConcurrentTests + { + [TestMethod] + [Ignore("This is to test middleware concurrency")] + public async Task TestFunctionAuthorizationMetadataCollectionAsync() + { + // Arrange + var client = new HttpClient { BaseAddress = new Uri("http://localhost:7005/") }; + + // Act + var message1 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); + var message2 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); + var message3 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); + var message4 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); + var request1 = client.SendAsync(message1); + var request2 = client.SendAsync(message2); + var request3 = client.SendAsync(message3); + var request4 = client.SendAsync(message4); + + // Assert + await Task.WhenAll(request1, request2, request3, request4); + Assert.AreEqual(HttpStatusCode.Unauthorized, request1.Result.StatusCode); + Assert.AreEqual(HttpStatusCode.Unauthorized, request2.Result.StatusCode); + Assert.AreEqual(HttpStatusCode.Unauthorized, request3.Result.StatusCode); + Assert.AreEqual(HttpStatusCode.Unauthorized, request4.Result.StatusCode); + } + } +}