Skip to content

Commit 20483e1

Browse files
Support storing AuthenticateResult in HttpContext/FunctionContext (#56)
* Basic support for getting AuthenticateResult from HttpContext feature * Cleanup, comments, and support for FunctionContext * Update ChangeLog and release version to match
1 parent eadba83 commit 20483e1

File tree

10 files changed

+227
-4
lines changed

10 files changed

+227
-4
lines changed

.build/release.props

Lines changed: 2 additions & 2 deletions
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.0</Version>
11+
<Version>4.1.1</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>

ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Change log
22
Change log stars with version 3.1.3
33

4+
## 4.1.1
5+
After authenticate but before authorize IAuthenticateResultFeature and IHttpAuthenticationFeature are now both set in HttpContext.Features and (for isolated Azure Functions) FunctionContext.Features.
6+
47
## 4.1.0
58
- ### [Breaking] Removing support for `Bearer` scheme and adding `FunctionsBearer`
69
Recent security updates in the Azure Functions runtime are clashing with the use of the default, well known `Bearer` scheme.<br/>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// <copyright file="FunctionAuthorizationFeature.cs" company="DarkLoop" author="Arturo Martinez">
2+
// Copyright (c) DarkLoop. All rights reserved.
3+
// </copyright>
4+
5+
using DarkLoop.Azure.Functions.Authorization.Internal;
6+
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.AspNetCore.Http.Features.Authentication;
8+
using System.Security.Claims;
9+
10+
namespace DarkLoop.Azure.Functions.Authorization
11+
{
12+
// This was designed with maximum compatibility with ASP.NET core. It keeps
13+
// two separate features in sync with each other automatically.
14+
internal sealed class FunctionAuthorizationFeature : IAuthenticateResultFeature, IHttpAuthenticationFeature
15+
{
16+
private ClaimsPrincipal? _principal;
17+
private AuthenticateResult? _authenticateResult;
18+
19+
/// <summary>
20+
/// Construct an instance of the feature with the given AuthenticateResult
21+
/// </summary>
22+
/// <param name="result"></param>
23+
public FunctionAuthorizationFeature(AuthenticateResult result)
24+
{
25+
Check.NotNull(result, nameof(result));
26+
27+
AuthenticateResult = result;
28+
}
29+
30+
/// <inheritdoc/>
31+
public AuthenticateResult? AuthenticateResult
32+
{
33+
get => _authenticateResult;
34+
set
35+
{
36+
_authenticateResult = value;
37+
_principal = value?.Principal;
38+
}
39+
}
40+
41+
/// <inheritdoc/>
42+
public ClaimsPrincipal? User
43+
{
44+
get => _principal;
45+
set
46+
{
47+
_authenticateResult = null;
48+
_principal = value;
49+
}
50+
}
51+
}
52+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// <copyright file="FunctionsFeatureCollectionExtension.cs" company="DarkLoop" author="Arturo Martinez">
2+
// Copyright (c) DarkLoop. All rights reserved.
3+
// </copyright>
4+
5+
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Http.Features;
7+
using Microsoft.AspNetCore.Http.Features.Authentication;
8+
9+
namespace DarkLoop.Azure.Functions.Authorization.Internal
10+
{
11+
// This functionality is used internally to emulate Asp.net's treatment of AuthenticateResult
12+
internal static class FunctionsFeatureCollectionExtension
13+
{
14+
/// <summary>
15+
/// Store the given AuthenticateResult in the IFeatureCollection accessible via
16+
/// IAuthenticateResultFeature and IHttpAuthenticationFeature
17+
/// </summary>
18+
/// <param name="features">The feature collection to add to</param>
19+
/// <param name="result">The authentication to expose in the feature collection</param>
20+
/// <returns>The object associated with the features</returns>
21+
public static FunctionAuthorizationFeature SetAuthenticationFeatures(this IFeatureCollection features, AuthenticateResult result)
22+
{
23+
// A single object is used to handle both of these features so that they stay in sync.
24+
// This is in line with what asp core normally does.
25+
var feature = new FunctionAuthorizationFeature(result);
26+
27+
features.Set<IAuthenticateResultFeature>(feature);
28+
features.Set<IHttpAuthenticationFeature>(feature);
29+
30+
return feature;
31+
}
32+
}
33+
}

src/in-proc/FunctionsAuthorizationExecutor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public async Task ExecuteAuthorizationAsync(FunctionExecutingContext context, Ht
7979

8080
var authenticateResult = await _policyEvaluator.AuthenticateAsync(filter.Policy, httpContext);
8181

82+
httpContext.Features.SetAuthenticationFeatures(authenticateResult);
83+
8284
// still authenticating in case token is sent to set context user but skipping authorization
8385
if (filter.AllowAnonymous)
8486
{

src/isolated/FunctionsAuthorizationMiddleware.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
using System.Threading.Tasks;
77
using DarkLoop.Azure.Functions.Authorization.Internal;
88
using DarkLoop.Azure.Functions.Authorization.Properties;
9+
using Microsoft.AspNetCore.Authentication;
910
using Microsoft.AspNetCore.Authorization;
1011
using Microsoft.AspNetCore.Authorization.Policy;
1112
using Microsoft.AspNetCore.Http.Extensions;
13+
using Microsoft.AspNetCore.Http.Features.Authentication;
1214
using Microsoft.Azure.Functions.Worker;
1315
using Microsoft.Azure.Functions.Worker.Middleware;
1416
using Microsoft.Extensions.Logging;
@@ -83,6 +85,12 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next
8385

8486
var authenticateResult = await _policyEvaluator.AuthenticateAsync(filter.Policy, httpContext);
8587

88+
var authenticateFeature = httpContext.Features.SetAuthenticationFeatures(authenticateResult);
89+
90+
// We also make the features available in the FunctionContext
91+
context.Features.Set<IAuthenticateResultFeature>(authenticateFeature);
92+
context.Features.Set<IHttpAuthenticationFeature>(authenticateFeature);
93+
8694
if (filter.AllowAnonymous)
8795
{
8896
await next(context);

test/Common.Tests/HttpUtils.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ public static HttpContext SetupHttpContext(IServiceProvider services)
2525
var streamReader = PipeReader.Create(requestStream);
2626
var requestHeaders = new HeaderDictionary();
2727
var streamWriter = PipeWriter.Create(responseStream);
28+
var features = new FeatureCollection();
2829

2930
httpContextMock.SetupGet(x => x.RequestServices).Returns(services);
3031
httpContextMock.SetupGet(x => x.Request).Returns(requestMock.Object);
3132
httpContextMock.SetupGet(x => x.Response).Returns(responseMock.Object);
32-
httpContextMock.SetupGet(x => x.Features).Returns(Mock.Of<IFeatureCollection>());
33+
httpContextMock.SetupGet(x => x.Features).Returns(features);
3334
httpContextMock.SetupGet(x => x.Items).Returns(new Dictionary<object, object?>());
3435
requestMock.SetupGet(x => x.RouteValues).Returns(new RouteValueDictionary());
3536
requestMock.SetupGet(x => x.Body).Returns(requestStream);
@@ -43,5 +44,6 @@ public static HttpContext SetupHttpContext(IServiceProvider services)
4344

4445
return httpContextMock.Object;
4546
}
47+
4648
}
4749
}

test/InProc.Tests/FunctionsAuthorizationExecutorTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.AspNetCore.Authorization.Policy;
1313
using Microsoft.AspNetCore.Http;
1414
using Microsoft.AspNetCore.Http.Extensions;
15+
using Microsoft.AspNetCore.Http.Features.Authentication;
1516
using Microsoft.Azure.WebJobs.Host;
1617
using Microsoft.Extensions.Configuration;
1718
using Microsoft.Extensions.DependencyInjection;
@@ -237,6 +238,46 @@ public async Task AuthorizationExecutorShouldThrowWhenFailedAuthenticationAndDoe
237238
policyEvaluatorMock.Verify(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()), Times.Once);
238239
}
239240

241+
[TestMethod("AuthorizationExecutor: should set http features on authenticate success or failure")]
242+
public async Task AuthorizationExecutorShouldSetHttpFeaturesOnSuccessOrFailure()
243+
{
244+
// Arrange
245+
var options = _services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>();
246+
options.CurrentValue.AuthorizationDisabled = false;
247+
248+
var authorizationProviderMock = new Mock<IFunctionsAuthorizationProvider>();
249+
authorizationProviderMock
250+
.Setup(provider => provider.GetAuthorizationAsync(It.IsAny<string>(), It.IsAny<IAuthorizationPolicyProvider>()))
251+
.ReturnsAsync(new FunctionAuthorizationFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(), false));
252+
253+
var policyEvaluatorMock = new Mock<IPolicyEvaluator>();
254+
policyEvaluatorMock
255+
.Setup(evaluator => evaluator.AuthenticateAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<HttpContext>()))
256+
.ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), "")));
257+
policyEvaluatorMock
258+
.Setup(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()))
259+
.ReturnsAsync(PolicyAuthorizationResult.Success());
260+
261+
var executor = new FunctionsAuthorizationExecutor(
262+
authorizationProviderMock.Object,
263+
_services!.GetRequiredService<IFunctionsAuthorizationResultHandler>(),
264+
_services!.GetRequiredService<IAuthorizationPolicyProvider>(),
265+
policyEvaluatorMock.Object,
266+
_services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>(),
267+
_services!.GetRequiredService<ILogger<FunctionsAuthorizationExecutor>>());
268+
269+
var functionId = Guid.NewGuid();
270+
var httpContext = HttpUtils.SetupHttpContext(_services!);
271+
var context = SetupExecutingContext(functionId, "TestFunction", httpContext.Request);
272+
273+
// Act
274+
await executor.ExecuteAuthorizationAsync(context, httpContext);
275+
276+
// Assert
277+
Assert.IsNotNull(httpContext.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
278+
Assert.IsNotNull(httpContext.Features.Get<IHttpAuthenticationFeature>()?.User);
279+
}
280+
240281
private static FunctionExecutingContext SetupExecutingContext(Guid functionId, string functionName, HttpRequest request)
241282
{
242283
var args = new Dictionary<string, object>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// <copyright file="FakeInvocationFeatures.cs" company="DarkLoop" author="Arturo Martinez">
2+
// Copyright (c) DarkLoop. All rights reserved.
3+
// </copyright>
4+
5+
using Microsoft.Azure.Functions.Worker;
6+
using System.Collections;
7+
8+
namespace Isolated.Tests.Fakes
9+
{
10+
internal sealed class FakeInvocationFeatures : IInvocationFeatures
11+
{
12+
private Dictionary<Type, object> _underlyingSet = new Dictionary<Type, object>();
13+
14+
public T? Get<T>()
15+
{
16+
_underlyingSet.TryGetValue(typeof(T), out var feature);
17+
18+
return (T?)feature;
19+
}
20+
21+
public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
22+
{
23+
return _underlyingSet.GetEnumerator();
24+
}
25+
26+
public void Set<T>(T instance)
27+
{
28+
_underlyingSet.Add(typeof(T), instance);
29+
}
30+
31+
IEnumerator IEnumerable.GetEnumerator()
32+
{
33+
return _underlyingSet.GetEnumerator();
34+
}
35+
}
36+
}

test/Isolated.Tests/FunctionsAuthorizationMiddlewareTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.AspNetCore.Authorization.Policy;
1313
using Microsoft.AspNetCore.Http;
1414
using Microsoft.AspNetCore.Http.Extensions;
15+
using Microsoft.AspNetCore.Http.Features.Authentication;
1516
using Microsoft.Azure.Functions.Worker;
1617
using Microsoft.Extensions.Configuration;
1718
using Microsoft.Extensions.DependencyInjection;
@@ -237,6 +238,50 @@ await middleware.Invoke(context, async fc =>
237238
policyEvaluatorMock.Verify(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()), Times.Once);
238239
}
239240

241+
[TestMethod("AuthorizationMiddleware: on success should set appropriate HttpContext features")]
242+
public async Task AuthorizationMiddlewareOnSuccessShouldSetAppropriateHttpContextFeatures()
243+
{
244+
// Arrange
245+
var authorizationProviderMock = new Mock<IFunctionsAuthorizationProvider>();
246+
authorizationProviderMock
247+
.Setup(provider => provider.GetAuthorizationAsync(It.IsAny<string>(), It.IsAny<IAuthorizationPolicyProvider>()))
248+
.ReturnsAsync(new FunctionAuthorizationFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(), false));
249+
250+
var policyEvaluatorMock = new Mock<IPolicyEvaluator>();
251+
policyEvaluatorMock
252+
.Setup(evaluator => evaluator.AuthenticateAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<HttpContext>()))
253+
.ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), "fakeauth")));
254+
policyEvaluatorMock
255+
.Setup(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()))
256+
.ReturnsAsync(PolicyAuthorizationResult.Success());
257+
258+
var middleware = new FunctionsAuthorizationMiddleware(
259+
authorizationProviderMock.Object,
260+
_services!.GetRequiredService<IFunctionsAuthorizationResultHandler>(),
261+
_services!.GetRequiredService<IAuthorizationPolicyProvider>(),
262+
policyEvaluatorMock.Object,
263+
_services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>(),
264+
_services!.GetRequiredService<ILogger<FunctionsAuthorizationMiddleware>>());
265+
266+
var functionId = "098039841";
267+
var entryPoint = $"{typeof(FakeFunctionClass).FullName}.TestFunction";
268+
var httpContext = HttpUtils.SetupHttpContext(_services!);
269+
var context = SetupFunctionContext(functionId, "TestFunction", entryPoint, "httpTrigger", "request", httpContext);
270+
271+
// Act
272+
await middleware.Invoke(context, async fc =>
273+
{
274+
await Task.CompletedTask;
275+
});
276+
277+
// Assert
278+
Assert.IsNotNull(httpContext.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
279+
Assert.IsNotNull(httpContext.Features.Get<IHttpAuthenticationFeature>()?.User);
280+
281+
Assert.IsNotNull(context.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
282+
Assert.IsNotNull(context.Features.Get<IHttpAuthenticationFeature>()?.User);
283+
}
284+
240285
private FunctionContext SetupFunctionContext(
241286
string functionId, string functionName, string entryPoint, string triggerType, string boundTriggerParamName, HttpContext? httpContext = null)
242287
{
@@ -254,10 +299,11 @@ private FunctionContext SetupFunctionContext(
254299
}
255300

256301
var context = new Mock<FunctionContext>();
302+
var features = new FakeInvocationFeatures();
257303
context.Setup(context => context.FunctionId).Returns(functionId);
258304
context.Setup(context => context.FunctionDefinition.Name).Returns(functionName);
259305
context.Setup(context => context.FunctionDefinition.EntryPoint).Returns(entryPoint);
260-
context.Setup(context => context.Features).Returns(Mock.Of<IInvocationFeatures>());
306+
context.Setup(context => context.Features).Returns(features);
261307
context.Setup(context => context.Items).Returns(items);
262308
context
263309
.Setup(contextMock => contextMock.FunctionDefinition.InputBindings)

0 commit comments

Comments
 (0)