Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .build/release.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<Company>DarkLoop</Company>
<Copyright>DarkLoop - All rights reserved</Copyright>
<Product>DarkLoop's Azure Functions Authorization</Product>
<IsPreview>false</IsPreview>
<IsPreview>true</IsPreview>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<Version>4.1.0</Version>
<Version>4.1.1</Version>
<FileVersion>$(Version).0</FileVersion>
<RepositoryUrl>https://github.com/dark-loop/functions-authorize</RepositoryUrl>
<License>https://github.com/dark-loop/functions-authorize/blob/master/LICENSE</License>
Expand Down
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change log
Change log stars with version 3.1.3

## 4.1.1
After authenticate but before authorize IAuthenticateResultFeature and IHttpAuthenticationFeature are now both set in HttpContext.Features and (for isolated Azure Functions) FunctionContext.Features.

## 4.1.0
- ### [Breaking] Removing support for `Bearer` scheme and adding `FunctionsBearer`
Recent security updates in the Azure Functions runtime are clashing with the use of the default, well known `Bearer` scheme.<br/>
Expand Down
52 changes: 52 additions & 0 deletions src/abstractions/FunctionAuthorizationFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// <copyright file="FunctionAuthorizationFeature.cs" company="DarkLoop" author="Arturo Martinez">
// Copyright (c) DarkLoop. All rights reserved.
// </copyright>

using DarkLoop.Azure.Functions.Authorization.Internal;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Features.Authentication;
using System.Security.Claims;

namespace DarkLoop.Azure.Functions.Authorization
{
// This was designed with maximum compatibility with ASP.NET core. It keeps
// two separate features in sync with each other automatically.
internal sealed class FunctionAuthorizationFeature : IAuthenticateResultFeature, IHttpAuthenticationFeature
{
private ClaimsPrincipal? _principal;
private AuthenticateResult? _authenticateResult;

/// <summary>
/// Construct an instance of the feature with the given AuthenticateResult
/// </summary>
/// <param name="result"></param>
public FunctionAuthorizationFeature(AuthenticateResult result)
{
Check.NotNull(result, nameof(result));

AuthenticateResult = result;
}

/// <inheritdoc/>
public AuthenticateResult? AuthenticateResult
{
get => _authenticateResult;
set
{
_authenticateResult = value;
_principal = value?.Principal;
}
}

/// <inheritdoc/>
public ClaimsPrincipal? User
{
get => _principal;
set
{
_authenticateResult = null;
_principal = value;
}
}
}
}
33 changes: 33 additions & 0 deletions src/abstractions/Internal/FunctionsFeatureCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// <copyright file="FunctionsFeatureCollectionExtension.cs" company="DarkLoop" author="Arturo Martinez">
// Copyright (c) DarkLoop. All rights reserved.
// </copyright>

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;

namespace DarkLoop.Azure.Functions.Authorization.Internal
{
// This functionality is used internally to emulate Asp.net's treatment of AuthenticateResult
internal static class FunctionsFeatureCollectionExtension
{
/// <summary>
/// Store the given AuthenticateResult in the IFeatureCollection accessible via
/// IAuthenticateResultFeature and IHttpAuthenticationFeature
/// </summary>
/// <param name="features">The feature collection to add to</param>
/// <param name="result">The authentication to expose in the feature collection</param>
/// <returns>The object associated with the features</returns>
public static FunctionAuthorizationFeature SetAuthenticationFeatures(this IFeatureCollection features, AuthenticateResult result)
{
// A single object is used to handle both of these features so that they stay in sync.
// This is in line with what asp core normally does.
var feature = new FunctionAuthorizationFeature(result);

features.Set<IAuthenticateResultFeature>(feature);
features.Set<IHttpAuthenticationFeature>(feature);

return feature;
}
}
}
2 changes: 2 additions & 0 deletions src/in-proc/FunctionsAuthorizationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public async Task ExecuteAuthorizationAsync(FunctionExecutingContext context, Ht

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

httpContext.Features.SetAuthenticationFeatures(authenticateResult);

// still authenticating in case token is sent to set context user but skipping authorization
if (filter.AllowAnonymous)
{
Expand Down
8 changes: 8 additions & 0 deletions src/isolated/FunctionsAuthorizationMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
using System.Threading.Tasks;
using DarkLoop.Azure.Functions.Authorization.Internal;
using DarkLoop.Azure.Functions.Authorization.Properties;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -83,6 +85,12 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next

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

var authenticateFeature = httpContext.Features.SetAuthenticationFeatures(authenticateResult);

// We also make the features available in the FunctionContext
context.Features.Set<IAuthenticateResultFeature>(authenticateFeature);
context.Features.Set<IHttpAuthenticationFeature>(authenticateFeature);

if (filter.AllowAnonymous)
{
await next(context);
Expand Down
4 changes: 3 additions & 1 deletion test/Common.Tests/HttpUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ public static HttpContext SetupHttpContext(IServiceProvider services)
var streamReader = PipeReader.Create(requestStream);
var requestHeaders = new HeaderDictionary();
var streamWriter = PipeWriter.Create(responseStream);
var features = new FeatureCollection();

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

return httpContextMock.Object;
}

}
}
41 changes: 41 additions & 0 deletions test/InProc.Tests/FunctionsAuthorizationExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -237,6 +238,46 @@ public async Task AuthorizationExecutorShouldThrowWhenFailedAuthenticationAndDoe
policyEvaluatorMock.Verify(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()), Times.Once);
}

[TestMethod("AuthorizationExecutor: should set http features on authenticate success or failure")]
public async Task AuthorizationExecutorShouldSetHttpFeaturesOnSuccessOrFailure()
{
// Arrange
var options = _services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>();
options.CurrentValue.AuthorizationDisabled = false;

var authorizationProviderMock = new Mock<IFunctionsAuthorizationProvider>();
authorizationProviderMock
.Setup(provider => provider.GetAuthorizationAsync(It.IsAny<string>(), It.IsAny<IAuthorizationPolicyProvider>()))
.ReturnsAsync(new FunctionAuthorizationFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(), false));

var policyEvaluatorMock = new Mock<IPolicyEvaluator>();
policyEvaluatorMock
.Setup(evaluator => evaluator.AuthenticateAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<HttpContext>()))
.ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), "")));
policyEvaluatorMock
.Setup(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()))
.ReturnsAsync(PolicyAuthorizationResult.Success());

var executor = new FunctionsAuthorizationExecutor(
authorizationProviderMock.Object,
_services!.GetRequiredService<IFunctionsAuthorizationResultHandler>(),
_services!.GetRequiredService<IAuthorizationPolicyProvider>(),
policyEvaluatorMock.Object,
_services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>(),
_services!.GetRequiredService<ILogger<FunctionsAuthorizationExecutor>>());

var functionId = Guid.NewGuid();
var httpContext = HttpUtils.SetupHttpContext(_services!);
var context = SetupExecutingContext(functionId, "TestFunction", httpContext.Request);

// Act
await executor.ExecuteAuthorizationAsync(context, httpContext);

// Assert
Assert.IsNotNull(httpContext.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
Assert.IsNotNull(httpContext.Features.Get<IHttpAuthenticationFeature>()?.User);
}

private static FunctionExecutingContext SetupExecutingContext(Guid functionId, string functionName, HttpRequest request)
{
var args = new Dictionary<string, object>
Expand Down
36 changes: 36 additions & 0 deletions test/Isolated.Tests/Fakes/FakeInvocationFeatures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// <copyright file="FakeInvocationFeatures.cs" company="DarkLoop" author="Arturo Martinez">
// Copyright (c) DarkLoop. All rights reserved.
// </copyright>

using Microsoft.Azure.Functions.Worker;
using System.Collections;

namespace Isolated.Tests.Fakes
{
internal sealed class FakeInvocationFeatures : IInvocationFeatures
{
private Dictionary<Type, object> _underlyingSet = new Dictionary<Type, object>();

public T? Get<T>()
{
_underlyingSet.TryGetValue(typeof(T), out var feature);

return (T?)feature;
}

public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
{
return _underlyingSet.GetEnumerator();
}

public void Set<T>(T instance)
{
_underlyingSet.Add(typeof(T), instance);
}

IEnumerator IEnumerable.GetEnumerator()
{
return _underlyingSet.GetEnumerator();
}
}
}
48 changes: 47 additions & 1 deletion test/Isolated.Tests/FunctionsAuthorizationMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -237,6 +238,50 @@ await middleware.Invoke(context, async fc =>
policyEvaluatorMock.Verify(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()), Times.Once);
}

[TestMethod("AuthorizationMiddleware: on success should set appropriate HttpContext features")]
public async Task AuthorizationMiddlewareOnSuccessShouldSetAppropriateHttpContextFeatures()
{
// Arrange
var authorizationProviderMock = new Mock<IFunctionsAuthorizationProvider>();
authorizationProviderMock
.Setup(provider => provider.GetAuthorizationAsync(It.IsAny<string>(), It.IsAny<IAuthorizationPolicyProvider>()))
.ReturnsAsync(new FunctionAuthorizationFilter(new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(), false));

var policyEvaluatorMock = new Mock<IPolicyEvaluator>();
policyEvaluatorMock
.Setup(evaluator => evaluator.AuthenticateAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<HttpContext>()))
.ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), "fakeauth")));
policyEvaluatorMock
.Setup(evaluator => evaluator.AuthorizeAsync(It.IsAny<AuthorizationPolicy>(), It.IsAny<AuthenticateResult>(), It.IsAny<HttpContext>(), It.IsAny<object>()))
.ReturnsAsync(PolicyAuthorizationResult.Success());

var middleware = new FunctionsAuthorizationMiddleware(
authorizationProviderMock.Object,
_services!.GetRequiredService<IFunctionsAuthorizationResultHandler>(),
_services!.GetRequiredService<IAuthorizationPolicyProvider>(),
policyEvaluatorMock.Object,
_services!.GetRequiredService<IOptionsMonitor<FunctionsAuthorizationOptions>>(),
_services!.GetRequiredService<ILogger<FunctionsAuthorizationMiddleware>>());

var functionId = "098039841";
var entryPoint = $"{typeof(FakeFunctionClass).FullName}.TestFunction";
var httpContext = HttpUtils.SetupHttpContext(_services!);
var context = SetupFunctionContext(functionId, "TestFunction", entryPoint, "httpTrigger", "request", httpContext);

// Act
await middleware.Invoke(context, async fc =>
{
await Task.CompletedTask;
});

// Assert
Assert.IsNotNull(httpContext.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
Assert.IsNotNull(httpContext.Features.Get<IHttpAuthenticationFeature>()?.User);

Assert.IsNotNull(context.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult);
Assert.IsNotNull(context.Features.Get<IHttpAuthenticationFeature>()?.User);
}

private FunctionContext SetupFunctionContext(
string functionId, string functionName, string entryPoint, string triggerType, string boundTriggerParamName, HttpContext? httpContext = null)
{
Expand All @@ -254,10 +299,11 @@ private FunctionContext SetupFunctionContext(
}

var context = new Mock<FunctionContext>();
var features = new FakeInvocationFeatures();
context.Setup(context => context.FunctionId).Returns(functionId);
context.Setup(context => context.FunctionDefinition.Name).Returns(functionName);
context.Setup(context => context.FunctionDefinition.EntryPoint).Returns(entryPoint);
context.Setup(context => context.Features).Returns(Mock.Of<IInvocationFeatures>());
context.Setup(context => context.Features).Returns(features);
context.Setup(context => context.Items).Returns(items);
context
.Setup(contextMock => contextMock.FunctionDefinition.InputBindings)
Expand Down