Skip to content

Commit f5020f3

Browse files
artmasaArturo Martinez
and
Arturo Martinez
authored
Refactor dependencies and improve thread safety (#65)
Removed unused JWT and Azure Functions Worker dependencies in `Program.cs` and `SampleIsolatedFunctions.V4.csproj`. Updated authentication scheme in `TestFunction.cs`. Improved thread safety in `FunctionsAuthorizationProvider.cs` and `FunctionsAuthorizationMetadataMiddleware.cs` using `string.Intern` and `KeyedMonitor`. Upgraded `Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore` to version `1.3.2`. Added `ConcurrentTests` to test middleware concurrency. Related issues: #62, #64 Co-authored-by: Arturo Martinez <[email protected]>
1 parent 81033d4 commit f5020f3

File tree

8 files changed

+80
-31
lines changed

8 files changed

+80
-31
lines changed

.build/release.props

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
<Company>DarkLoop</Company>
77
<Copyright>DarkLoop - All rights reserved</Copyright>
88
<Product>DarkLoop's Azure Functions Authorization</Product>
9-
<IsPreview>false</IsPreview>
9+
<IsPreview>true</IsPreview>
1010
<AssemblyVersion>4.0.0.0</AssemblyVersion>
11-
<Version>4.1.1</Version>
11+
<Version>4.1.2</Version>
1212
<FileVersion>$(Version).0</FileVersion>
1313
<RepositoryUrl>https://github.com/dark-loop/functions-authorize</RepositoryUrl>
1414
<License>https://github.com/dark-loop/functions-authorize/blob/master/LICENSE</License>

sample/SampleIsolatedFunctions.V4/Program.cs

-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
// Copyright (c) DarkLoop. All rights reserved.
33
// </copyright>
44

5-
using System.IdentityModel.Tokens.Jwt;
65
using Common.Tests;
76
using DarkLoop.Azure.Functions.Authorization;
8-
using Microsoft.AspNetCore.Authentication.JwtBearer;
97
using Microsoft.Azure.Functions.Worker;
108
using Microsoft.Extensions.DependencyInjection;
119
using Microsoft.Extensions.Hosting;

sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj

-4
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
</PropertyGroup>
1010
<ItemGroup>
1111
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12-
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
13-
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.20.1" />
14-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
15-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.2.0" />
1612
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.1.0" />
1713
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.16.4" />
1814
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />

sample/SampleIsolatedFunctions.V4/TestFunction.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace SampleIsolatedFunctions.V4
1616
{
17-
[FunctionAuthorize(AuthenticationSchemes = "Bearer")]
17+
[FunctionAuthorize(AuthenticationSchemes = "FunctionsBearer")]
1818
public class TestFunction
1919
{
2020
private readonly ILogger<TestFunction> _logger;

src/abstractions/FunctionsAuthorizationProvider.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public async Task<FunctionAuthorizationFilter> GetAuthorizationAsync(string func
7070
return filter!;
7171
}
7272

73-
var asyncKey = $"fap:{functionName}";
74-
73+
// ensuring key is interned before entering monitor since key is compared as object
74+
var asyncKey = string.Intern($"fap:{functionName}");
7575
await KeyedMonitor.EnterAsync(asyncKey, unblockOnFirstExit: true);
7676

7777
try

src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@
2121

2222
<ItemGroup>
2323
<FrameworkReference Include="Microsoft.AspNetCore.App" />
24-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Core" Version="1.15.0" />
2524
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.3.0" />
26-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.1.0" />
25+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.3.2" />
2726
</ItemGroup>
2827

2928
<ItemGroup>

src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs

+37-18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading.Tasks;
1010
using DarkLoop.Azure.Functions.Authorization.Extensions;
1111
using DarkLoop.Azure.Functions.Authorization.Features;
12+
using DarkLoop.Azure.Functions.Authorization.Internal;
1213
using Microsoft.AspNetCore.Authorization;
1314
using Microsoft.Azure.Functions.Worker;
1415
using Microsoft.Azure.Functions.Worker.Middleware;
@@ -39,9 +40,9 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next
3940
return;
4041
}
4142

42-
if(!_options.IsFunctionRegistered(context.FunctionDefinition.Name))
43+
if (!_options.IsFunctionRegistered(context.FunctionDefinition.Name))
4344
{
44-
RegisterHttpTriggerAuthorization(context);
45+
await RegisterHttpTriggerAuthorizationAsync(context);
4546
}
4647

4748
context.Features.Set<IFunctionsAuthorizationFeature>(
@@ -50,27 +51,45 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next
5051
await next(context);
5152
}
5253

53-
private void RegisterHttpTriggerAuthorization(FunctionContext context)
54+
private async Task RegisterHttpTriggerAuthorizationAsync(FunctionContext context)
5455
{
55-
var functionName = context.FunctionDefinition.Name;
56-
var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch
56+
// Middleware can be hit concurrently, we need to ensure this functionality
57+
// is thread-safe on a per function basis.
58+
// Ensuring key is interned before entering monitor since key is compared as object
59+
var monitorKey = string.Intern($"famm:{context.FunctionId}");
60+
await KeyedMonitor.EnterAsync(monitorKey);
61+
62+
try
5763
{
58-
-1 => string.Empty,
59-
var index => context.FunctionDefinition.EntryPoint[..index]
60-
};
64+
if (_options.IsFunctionRegistered(context.FunctionDefinition.Name))
65+
{
66+
return;
67+
}
68+
69+
var functionName = context.FunctionDefinition.Name;
70+
var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch
71+
{
72+
-1 => string.Empty,
73+
var index => context.FunctionDefinition.EntryPoint[..index]
74+
};
6175

62-
var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..];
63-
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
64-
var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false))
65-
.FirstOrDefault(t => t is not null)?
66-
.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ??
67-
throw new MethodAccessException(
68-
$"Method instance for function '{context.FunctionDefinition.Name}' " +
69-
$"cannot be found or cannot be accessed due to its protection level.");
76+
var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..];
77+
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
78+
var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false))
79+
.FirstOrDefault(t => t is not null)?
80+
.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ??
81+
throw new MethodAccessException(
82+
$"Method instance for function '{context.FunctionDefinition.Name}' " +
83+
$"cannot be found or cannot be accessed due to its protection level.");
7084

71-
var declaringType = method.DeclaringType!;
85+
var declaringType = method.DeclaringType!;
7286

73-
_options.RegisterFunctionAuthorizationAttributesMetadata<AuthorizeAttribute>(functionName, declaringType, method);
87+
_options.RegisterFunctionAuthorizationAttributesMetadata<AuthorizeAttribute>(functionName, declaringType, method);
88+
}
89+
finally
90+
{
91+
KeyedMonitor.Exit(monitorKey);
92+
}
7493
}
7594
}
7695
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// <copyright file="IntegrationTests.cs" company="DarkLoop" author="Arturo Martinez">
2+
// Copyright (c) DarkLoop. All rights reserved.
3+
// </copyright>
4+
5+
using System.Net;
6+
7+
namespace Isolated.Tests
8+
{
9+
[TestClass]
10+
public class ConcurrentTests
11+
{
12+
[TestMethod]
13+
[Ignore("This is to test middleware concurrency")]
14+
public async Task TestFunctionAuthorizationMetadataCollectionAsync()
15+
{
16+
// Arrange
17+
var client = new HttpClient { BaseAddress = new Uri("http://localhost:7005/") };
18+
19+
// Act
20+
var message1 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction");
21+
var message2 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction");
22+
var message3 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction");
23+
var message4 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction");
24+
var request1 = client.SendAsync(message1);
25+
var request2 = client.SendAsync(message2);
26+
var request3 = client.SendAsync(message3);
27+
var request4 = client.SendAsync(message4);
28+
29+
// Assert
30+
await Task.WhenAll(request1, request2, request3, request4);
31+
Assert.AreEqual(HttpStatusCode.Unauthorized, request1.Result.StatusCode);
32+
Assert.AreEqual(HttpStatusCode.Unauthorized, request2.Result.StatusCode);
33+
Assert.AreEqual(HttpStatusCode.Unauthorized, request3.Result.StatusCode);
34+
Assert.AreEqual(HttpStatusCode.Unauthorized, request4.Result.StatusCode);
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)