Skip to content

Commit c3b2889

Browse files
authored
Merge pull request #6 from fmichellonet/features/Authorize_on_roles
Support role and policy
2 parents aa8a116 + 059de84 commit c3b2889

13 files changed

+263
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Security.Claims;
4+
using System.Threading.Tasks;
5+
using AzureFunctions.Extensions.OpenIDConnect.Configuration;
6+
using FluentAssertions;
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Authorization.Infrastructure;
9+
using Microsoft.Azure.WebJobs.Host;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using NSubstitute;
12+
using NUnit.Framework;
13+
14+
namespace AzureFunctions.Extensions.OpenIDConnect.Tests
15+
{
16+
[TestFixture]
17+
public class AuthorizationServiceShould
18+
{
19+
20+
[TestCase("locale", "fr", "fr", true)]
21+
[TestCase("locale", "it", "fr", false)]
22+
public async Task ApplyPolicyRequirements(string claimType, string claimValue, string claimRequiredValue, bool isAuthorized)
23+
{
24+
// Arrange
25+
var policyName = "my policy";
26+
var localeClaim = new Claim(claimType, claimValue);
27+
28+
var collection = ServiceCollectionFixture.MinimalAzFunctionsServices();
29+
30+
collection.AddOpenIDConnect(builder =>
31+
{
32+
builder.SetIssuerBaseUrlConfiguration("https://issuer.com/");
33+
builder.SetTokenValidation("issuer", "audience");
34+
builder.SetTypeCrawler(() => new Type[] { });
35+
builder.AddPolicy(policyName, policy => policy.Requirements.Add(new ClaimsAuthorizationRequirement(claimType, new []{ claimRequiredValue }) ));
36+
});
37+
38+
var provider = collection.BuildServiceProvider();
39+
40+
var retriever = provider.GetService<IAuthorizationRequirementsRetriever>();
41+
var authorizationService = provider.GetService<IAuthorizationService>();
42+
43+
var user = Substitute.For<ClaimsPrincipal>();
44+
user.Claims.Returns(new List<Claim> {localeClaim});
45+
46+
47+
// Act
48+
var requirements = retriever.ForAttribute(new AuthorizeAttribute(policyName));
49+
var authResult = await authorizationService.AuthorizeAsync(user, null, requirements);
50+
51+
// Assert
52+
authResult.Succeeded.Should().Be(isAuthorized);
53+
}
54+
}
55+
}

src/AzureFunctions.Extensions.OpenIDConnect.Tests/AzureFunctions.Extensions.OpenIDConnect.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackageReference Include="FluentAssertions" Version="5.10.3" />
99
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.25" />
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
11+
<PackageReference Include="NSubstitute" Version="4.2.2" />
1112
<PackageReference Include="NUnit" Version="3.13.1" />
1213
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
1314
</ItemGroup>

src/AzureFunctions.Extensions.OpenIDConnect.Tests/DependencyInjectionShould.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class DependencyInjectionShould
1313
[Test]
1414
public void Be_Resolvable()
1515
{
16-
var collection = new ServiceCollection();
16+
var collection = ServiceCollectionFixture.MinimalAzFunctionsServices();
1717
collection.AddOpenIDConnect(builder =>
1818
{
1919
builder.SetIssuerBaseUrlConfiguration("https://issuer.com/");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using FluentAssertions;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Authorization.Infrastructure;
5+
using NUnit.Framework;
6+
7+
namespace AzureFunctions.Extensions.OpenIDConnect.Tests
8+
{
9+
[TestFixture]
10+
public class PolicyBuilderShould
11+
{
12+
[Test]
13+
public void Register_Created_Policies()
14+
{
15+
// Arrange
16+
var claim = new ClaimsAuthorizationRequirement("locale", new[] {"fr"});
17+
Action<AuthorizationPolicyBuilder> configurePolicy = policyBuilder => policyBuilder.Requirements.Add(claim);
18+
var policyBuilder = new AuthorizationPolicyBuilder();
19+
20+
// Act
21+
configurePolicy(policyBuilder);
22+
var res = policyBuilder.Build();
23+
24+
// Assert
25+
res.Requirements.Should().HaveCount(1);
26+
res.Requirements.Should().BeEquivalentTo(claim);
27+
}
28+
}
29+
}

src/AzureFunctions.Extensions.OpenIDConnect.Tests/RouteGuardianShould.cs

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Threading.Tasks;
2-
using FluentAssertions;
1+
using FluentAssertions;
32
using Microsoft.AspNetCore.Authorization;
43
using Microsoft.AspNetCore.Http;
54
using Microsoft.AspNetCore.Mvc;
@@ -17,65 +16,65 @@ namespace AzureFunctions.Extensions.OpenIDConnect.Tests
1716
public class RouteGuardianShould
1817
{
1918
[Test]
20-
public async Task Not_Authorize_When_Not_HttpTrigger()
19+
public void Not_Authorize_When_Not_HttpTrigger()
2120
{
2221
// Arrange
2322
var guardian = new RouteGuardian(() => new List<Type>{ typeof(Not_HttpTrigger) });
2423

2524
// Act
26-
var result = await guardian.ShouldAuthorize("Not_HttpTrigger");
25+
var result = guardian.IsProtectedRoute("Not_HttpTrigger");
2726

2827
// Assert
2928
result.Should().Be(false);
3029
}
3130

3231
[Test]
33-
public async Task Not_Authorize_When_No_Authorize_Attribute_On_Method_And_Type()
32+
public void Not_Authorize_When_No_Authorize_Attribute_On_Method_And_Type()
3433
{
3534
// Arrange
3635
var guardian = new RouteGuardian(() => new List<Type> { typeof(No_Authorize_Attribute_On_Method_And_Type) });
3736

3837
// Act
39-
var result = await guardian.ShouldAuthorize("No_Authorize_Attribute_On_Method_And_Type");
38+
var result = guardian.IsProtectedRoute("No_Authorize_Attribute_On_Method_And_Type");
4039

4140
// Assert
4241
result.Should().Be(false);
4342
}
4443

4544
[Test]
46-
public async Task Authorize_When_Authorize_Attribute_Is_On_Method()
45+
public void Authorize_When_Authorize_Attribute_Is_On_Method()
4746
{
4847
// Arrange
4948
var guardian = new RouteGuardian(() => new List<Type> { typeof(Authorize_Attribute_Is_On_Method) });
5049

5150
// Act
52-
var result = await guardian.ShouldAuthorize("Authorize_Attribute_Is_On_Method");
51+
var result = guardian.IsProtectedRoute("Authorize_Attribute_Is_On_Method");
5352

5453
// Assert
5554
result.Should().Be(true);
5655
}
5756

5857
[Test]
59-
public async Task Authorize_When_Authorize_Attribute_Is_On_Class()
58+
public void Authorize_When_Authorize_Attribute_Is_On_Class()
6059
{
6160
// Arrange
6261
var guardian = new RouteGuardian(() => new List<Type> { typeof(Authorize_Attribute_Is_On_Class) });
6362

6463
// Act
65-
var result = await guardian.ShouldAuthorize("Authorize_Attribute_Is_On_Class");
64+
var result = guardian.IsProtectedRoute("Authorize_Attribute_Is_On_Class");
6665

6766
// Assert
6867
result.Should().Be(true);
6968
}
7069

7170
[Test]
72-
public async Task NotAuthorize_When_Authorize_Attribute_Is_On_Class_But_AllowAnonimous_On_Method()
71+
public void NotAuthorize_When_Authorize_Attribute_Is_On_Class_But_AllowAnonimous_On_Method()
7372
{
7473
// Arrange
7574
var guardian = new RouteGuardian(() => new List<Type> { typeof(Attribute_Is_On_Class_But_AllowAnonimous_On_Method) });
7675

7776
// Act
78-
var result = await guardian.ShouldAuthorize("Attribute_Is_On_Class_But_AllowAnonimous_On_Method");
77+
var result = guardian.IsProtectedRoute("Attribute_Is_On_Class_But_AllowAnonimous_On_Method");
7978

8079
// Assert
8180
result.Should().Be(false);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
5+
using NSubstitute;
6+
7+
namespace AzureFunctions.Extensions.OpenIDConnect.Tests
8+
{
9+
public static class ServiceCollectionFixture
10+
{
11+
public static ServiceCollection MinimalAzFunctionsServices(ServiceCollection collection = null)
12+
{
13+
collection ??= new ServiceCollection();
14+
15+
collection.AddSingleton<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>();
16+
collection.AddSingleton<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>();
17+
18+
var authorizationOptions = Substitute.For<IOptions<AuthorizationOptions>>();
19+
authorizationOptions.Value.Returns(new AuthorizationOptions { InvokeHandlersAfterFailure = false });
20+
collection.AddSingleton(authorizationOptions);
21+
22+
var logger = Substitute.For<ILogger<DefaultAuthorizationService>>();
23+
collection.AddSingleton<ILogger<DefaultAuthorizationService>>(logger);
24+
25+
return collection;
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Authorization.Infrastructure;
5+
6+
namespace AzureFunctions.Extensions.OpenIDConnect
7+
{
8+
public class AuthorizationRequirementsRetriever : IAuthorizationRequirementsRetriever
9+
{
10+
private readonly IDictionary<string, AuthorizationPolicy> _policyMap;
11+
12+
public AuthorizationRequirementsRetriever(IDictionary<string, AuthorizationPolicy> policyMap)
13+
{
14+
_policyMap = policyMap;
15+
}
16+
17+
public IEnumerable<IAuthorizationRequirement> ForAttribute(AuthorizeAttribute attribute)
18+
{
19+
if (!string.IsNullOrEmpty(attribute.Roles))
20+
{
21+
return ForRoleAttribute(attribute);
22+
}
23+
24+
if (!string.IsNullOrEmpty(attribute.Policy))
25+
{
26+
return ForPolicyAttribute(attribute);
27+
}
28+
29+
return null;
30+
}
31+
32+
private IEnumerable<IAuthorizationRequirement> ForRoleAttribute(AuthorizeAttribute attribute)
33+
{
34+
var requirements = new List<RolesAuthorizationRequirement>
35+
{
36+
new RolesAuthorizationRequirement(attribute.Roles.Split(','))
37+
};
38+
39+
return requirements;
40+
}
41+
42+
private IEnumerable<IAuthorizationRequirement> ForPolicyAttribute(AuthorizeAttribute attribute)
43+
{
44+
if (!_policyMap.ContainsKey(attribute.Policy))
45+
{
46+
throw new ArgumentException($"{attribute.Policy} policy has not been registered");
47+
}
48+
49+
return _policyMap[attribute.Policy].Requirements;
50+
}
51+
}
52+
}

src/AzureFunctions.Extensions.OpenIDConnect/AuthorizeFilter.cs

+35-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace AzureFunctions.Extensions.OpenIDConnect
1+
using Microsoft.AspNetCore.Authorization;
2+
3+
namespace AzureFunctions.Extensions.OpenIDConnect
24
{
35
using System.Net;
46
using System.Threading;
@@ -11,28 +13,49 @@ public class AuthorizeFilter : FunctionInvocationFilterAttribute
1113
private readonly IHttpContextAccessor _httpContextAccessor;
1214
private readonly IAuthenticationService _authenticationService;
1315
private readonly IRouteGuardian _routeGuardian;
16+
private readonly IAuthorizationService _authorizationService;
17+
private readonly IAuthorizationRequirementsRetriever _requirementsRetriever;
1418

15-
public AuthorizeFilter(IHttpContextAccessor httpContextAccessor, IAuthenticationService authenticationService, IRouteGuardian routeGuardian)
19+
public AuthorizeFilter(IHttpContextAccessor httpContextAccessor,
20+
IAuthenticationService authenticationService, IRouteGuardian routeGuardian,
21+
IAuthorizationService authorizationService, IAuthorizationRequirementsRetriever requirementsRetriever)
1622
{
1723
_httpContextAccessor = httpContextAccessor;
1824
_authenticationService = authenticationService;
1925
_routeGuardian = routeGuardian;
26+
_authorizationService = authorizationService;
27+
_requirementsRetriever = requirementsRetriever;
2028
}
2129

2230
public override async Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
2331
{
24-
if (await _routeGuardian.ShouldAuthorize(executingContext.FunctionName))
32+
if (_routeGuardian.IsProtectedRoute(executingContext.FunctionName))
2533
{
2634
var httpContext = _httpContextAccessor.HttpContext;
2735

2836
// Authenticate the user
29-
var authResult = await _authenticationService.AuthenticateAsync(httpContext.Request.Headers);
30-
31-
if (authResult.Failed)
37+
var authenticationResult = await _authenticationService.AuthenticateAsync(httpContext.Request.Headers);
38+
39+
if (authenticationResult.Failed)
3240
{
3341
await Unauthorized(httpContext, cancellationToken);
3442
return;
3543
}
44+
45+
httpContext.User = authenticationResult.User;
46+
47+
var attribute = _routeGuardian.GetAuthorizationConfiguration(executingContext.FunctionName);
48+
var requirements = _requirementsRetriever.ForAttribute(attribute);
49+
50+
if (requirements != null)
51+
{
52+
var authorizationResult = await _authorizationService.AuthorizeAsync(httpContext.User, null, requirements);
53+
if (!authorizationResult.Succeeded)
54+
{
55+
await Forbidden(httpContext, authorizationResult.Failure, cancellationToken);
56+
}
57+
}
58+
3659
}
3760
await base.OnExecutingAsync(executingContext, cancellationToken);
3861
}
@@ -42,5 +65,11 @@ private async Task Unauthorized(HttpContext httpContext, CancellationToken cance
4265
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
4366
await httpContext.Response.WriteAsync(string.Empty, cancellationToken);
4467
}
68+
69+
private async Task Forbidden(HttpContext httpContext, AuthorizationFailure failure, CancellationToken cancellationToken)
70+
{
71+
httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
72+
await httpContext.Response.WriteAsync(failure.FailedRequirements.ToString(), cancellationToken);
73+
}
4574
}
4675
}

0 commit comments

Comments
 (0)