Skip to content

Commit 4e90fb6

Browse files
authored
Merge pull request #6 from dark-loop/security
Adding built-in auth to extension methods to address #5. This should enable setting up authentication at startup and not breaking access to functions in Azure portal as described in Azure/azure-functions-host#6959
2 parents 3f4a285 + 9c47cb3 commit 4e90fb6

14 files changed

+348
-101
lines changed

.build/release.props

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22

33
<PropertyGroup>
44
<Authors>Arturo Martinez</Authors>
5+
<Company>DarkLoop</Company>
56
<PackageId>DarkLoop.Azure.Functions.Authorize</PackageId>
67
<IsPreview>true</IsPreview>
78
<AssemblyVersion>3.0.0.0</AssemblyVersion>
8-
<Version>3.0.12</Version>
9+
<Version>3.1.0</Version>
910
<FileVersion>$(Version).0</FileVersion>
1011
<RepositoryUrl>https://github.com/dark-loop/functions-authorize</RepositoryUrl>
1112
<License>https://github.com/dark-loop/functions-authorize/blob/master/LICENSE</License>
1213
<RepositoryType>Git</RepositoryType>
13-
<PackageTags>AuthorizeAttribute, Authorize, Azure Functions</PackageTags>
14+
<PackageTags>AuthorizeAttribute, Authorize, Azure Functions, Azure, Bearer, JWT</PackageTags>
1415
<PackageIconUrl>https://en.gravatar.com/userimage/22176525/45f25acea686a783e5b2ca172d72db71.png</PackageIconUrl>
1516
<SignAssembly>true</SignAssembly>
1617
<AssemblyOriginatorKeyFile>dl-sftwr-sn-key.snk</AssemblyOriginatorKeyFile>
1718
<IsPackable>true</IsPackable>
19+
<Description>Azure Functions V3 authentication extensions to enable authentication and authorization on a per function basis.</Description>
1820
</PropertyGroup>
1921

2022
<PropertyGroup>

sample/DarkLoop.Azure.Functions.Authorize.SampleFunctions/DarkLoop.Azure.Functions.Authorize.SampleFunctions.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<UserSecretsId>51dc0b9d-8e74-45ec-aebc-1d3d6934faf5</UserSecretsId>
66
</PropertyGroup>
77
<ItemGroup>
8-
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.1" />
8+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.18" />
99
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="2.2.0" />
1010
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.0" />
1111
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.12" />

sample/DarkLoop.Azure.Functions.Authorize.SampleFunctions/Function1.cs

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
using System.IO;
2+
using System.Reflection;
3+
using System.Text;
24
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authentication;
36
using Microsoft.AspNetCore.Http;
47
using Microsoft.AspNetCore.Mvc;
58
using Microsoft.Azure.WebJobs;
69
using Microsoft.Azure.WebJobs.Extensions.Http;
10+
using Microsoft.Extensions.DependencyInjection;
711
using Microsoft.Extensions.Logging;
812
using Newtonsoft.Json;
913

@@ -17,19 +21,17 @@ public static async Task<IActionResult> Run(
1721
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
1822
ILogger log)
1923
{
20-
log.LogInformation("C# HTTP trigger function processed a request.");
24+
var provider = req.HttpContext.RequestServices;
25+
var schProvider = provider.GetService<IAuthenticationSchemeProvider>();
2126

22-
string name = req.Query["name"];
27+
var sb = new StringBuilder();
28+
foreach (var scheme in await schProvider.GetAllSchemesAsync())
29+
sb.AppendLine($"{scheme.Name} -> {scheme.HandlerType}");
2330

24-
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
25-
dynamic data = JsonConvert.DeserializeObject(requestBody);
26-
name = name ?? data?.name;
31+
sb.AppendLine();
32+
sb.AppendLine(Assembly.GetEntryAssembly().FullName);
2733

28-
string responseMessage = string.IsNullOrEmpty(name)
29-
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
30-
: $"Hello, {name}. This HTTP triggered function executed successfully.";
31-
32-
return new OkObjectResult(responseMessage);
34+
return new OkObjectResult(sb.ToString());
3335
}
3436
}
3537
}

sample/DarkLoop.Azure.Functions.Authorize.SampleFunctions/Startup.cs

+8-6
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class Startup : FunctionsStartup
1818

1919
public override void Configure(IFunctionsHostBuilder builder)
2020
{
21-
builder
22-
.AddAuthentication(options=>
21+
builder.Services
22+
.AddFunctionsAuthentication(options =>
2323
{
2424
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
2525
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -34,10 +34,12 @@ public override void Configure(IFunctionsHostBuilder builder)
3434
{
3535
OnAuthenticationFailed = async x =>
3636
{
37+
var body = "Unauthorized request";
3738
var response = x.Response;
3839
response.ContentType = "text/plain";
39-
response.ContentLength = 5;
40-
await response.WriteAsync("No go");
40+
response.ContentLength = body.Length;
41+
response.StatusCode = 401;
42+
await response.WriteAsync(body);
4143
await response.Body.FlushAsync();
4244
},
4345
OnChallenge = async x =>
@@ -51,9 +53,9 @@ public override void Configure(IFunctionsHostBuilder builder)
5153
//await response.Body.FlushAsync();
5254
}
5355
};
54-
});
56+
}, true);
5557

56-
builder.AddAuthorization();
58+
builder.Services.AddFunctionsAuthorization();
5759
}
5860

5961
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)

src/DarkLoop.Azure.Functions.Authorize/Constants.cs

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ internal class Constants
88
{
99
internal const string AuthInvokedKey = "__WebJobAuthInvoked";
1010
internal const string WebJobsAuthScheme = "WebJobsAuthLevel";
11+
internal const string ArmTokenAuthScheme = "ArmToken";
1112
}
1213
}

src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionsAuthorizeFilter.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ namespace DarkLoop.Azure.Functions.Authorize.Filters
1313
{
1414
internal class FunctionsAuthorizeFilter : IFunctionsAuthorizeFilter
1515
{
16+
private static readonly IEnumerable<string> __dismissedSchemes =
17+
AuthHelper.EnableAuth ?
18+
new[] { Constants.WebJobsAuthScheme } :
19+
new[] { Constants.WebJobsAuthScheme, Constants.ArmTokenAuthScheme };
20+
1621
public IEnumerable<IAuthorizeData> AuthorizeData { get; }
1722

1823
public IAuthenticationSchemeProvider SchemeProvider { get; }
@@ -39,7 +44,7 @@ private void IntegrateSchemes()
3944
var schemes = this.SchemeProvider.GetAllSchemesAsync().GetAwaiter().GetResult();
4045
var strSchemes = string.Join(',',
4146
from scheme in schemes
42-
where scheme.Name != Constants.WebJobsAuthScheme
47+
where !__dismissedSchemes.Contains(scheme.Name)
4348
select scheme.Name);
4449

4550
foreach (var data in this.AuthorizeData)

src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizeAttribute.cs

+12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace DarkLoop.Azure.Functions.Authorize
1313
{
14+
/// <summary>
15+
/// Represents authorization logic that needs to be applied to a function.
16+
/// </summary>
1417
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
1518
[Obsolete("This class is dependent on Azure Functions preview features.")]
1619
public class FunctionAuthorizeAttribute : FunctionInvocationFilterAttribute, IFunctionInvocationFilter, IAuthorizeData
@@ -22,10 +25,19 @@ public FunctionAuthorizeAttribute(string policy)
2225
this.Policy = policy;
2326
}
2427

28+
/// <summary>
29+
/// Gets or sets the name of the authorization policy to apply to function.
30+
/// </summary>
2531
public string? Policy { get; set; }
2632

33+
/// <summary>
34+
/// Gets or sets a comma separated list of roles that are required to execute function.
35+
/// </summary>
2736
public string? Roles { get; set; }
2837

38+
/// <summary>
39+
/// Gets or sets a comma separated list of authentication schemes that are required to apply the authorization logic.
40+
/// </summary>
2941
public string? AuthenticationSchemes { get; set; }
3042

3143
async Task IFunctionInvocationFilter.OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)

src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthExtension.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class FunctionsAuthExtension : IExtensionConfigProvider
88
{
99
public void Initialize(ExtensionConfigContext context)
1010
{
11-
11+
1212
}
1313
}
1414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.AspNetCore.Authentication;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using System;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
7+
namespace DarkLoop.Azure.Functions.Authorize.Security
8+
{
9+
internal class AuthHelper
10+
{
11+
protected static Assembly ScriptWebHostAssembly = Assembly.Load("Microsoft.Azure.WebJobs.Script.WebHost");
12+
protected static Type WebHostSvcCollectionExtType =
13+
ScriptWebHostAssembly.GetType("Microsoft.Azure.WebJobs.Script.WebHost.WebHostServiceCollectionExtensions");
14+
private static Type JwtSecurityExtsType =
15+
ScriptWebHostAssembly.GetType("Microsoft.Extensions.DependencyInjection.ScriptJwtBearerExtensions");
16+
private static Func<AuthenticationBuilder, AuthenticationBuilder> __func = BuildFunc();
17+
private static Func<IServiceCollection, IServiceCollection> __authorizationFunc = BuildAuthorizationFunc();
18+
19+
internal static bool EnableAuth { get; private set; }
20+
21+
static AuthHelper()
22+
{
23+
var entry = Assembly.GetEntryAssembly();
24+
var fullName = entry.FullName;
25+
var name = fullName.Substring(0, fullName.IndexOf(','));
26+
EnableAuth = name.Equals("Microsoft.Azure.WebJobs.Script.WebHost", StringComparison.OrdinalIgnoreCase);
27+
}
28+
29+
private static Func<AuthenticationBuilder, AuthenticationBuilder> BuildFunc()
30+
{
31+
var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
32+
var method = Expression.Call(JwtSecurityExtsType, "AddScriptJwtBearer", Type.EmptyTypes, builder);
33+
var lambda = Expression.Lambda<Func<AuthenticationBuilder, AuthenticationBuilder>>(method, builder);
34+
35+
return lambda.Compile();
36+
}
37+
38+
private static Func<IServiceCollection, IServiceCollection> BuildAuthorizationFunc()
39+
{
40+
var services = Expression.Parameter(typeof(IServiceCollection), "services");
41+
var method = Expression.Call(WebHostSvcCollectionExtType, "AddWebJobsScriptHostAuthorization", Type.EmptyTypes, services);
42+
var lambda = Expression.Lambda<Func<IServiceCollection, IServiceCollection>>(method, services);
43+
44+
return lambda.Compile();
45+
}
46+
47+
internal static IServiceCollection AddFunctionsBuiltInAuthorization(IServiceCollection services)
48+
{
49+
return __authorizationFunc(services);
50+
}
51+
52+
internal static FunctionsAuthenticationBuilder AddScriptJwtBearer(FunctionsAuthenticationBuilder builder)
53+
{
54+
__func(builder);
55+
return builder;
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,81 @@
11
using System;
2-
using System.Security.Claims;
3-
using System.Text;
4-
using System.Threading.Tasks;
5-
using DarkLoop.Azure.Functions.Authorize;
2+
using DarkLoop.Azure.Functions.Authorize.Security;
63
using Microsoft.AspNetCore.Authentication;
7-
using Microsoft.AspNetCore.Authentication.JwtBearer;
84
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
9-
using Microsoft.Azure.WebJobs.Extensions.Http;
105
using Microsoft.Extensions.Options;
11-
using Microsoft.IdentityModel.Tokens;
126

137
namespace Microsoft.Extensions.DependencyInjection
148
{
159
public static class AuthenticationExtensions
1610
{
17-
private const string AuthLevelClaimType = "http://schemas.microsoft.com/2017/07/functions/claims/authlevel";
11+
/// <summary>
12+
/// Adds Functions built-in authentication.
13+
/// </summary>
14+
public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(this IServiceCollection services)
15+
{
16+
if (services is null) throw new ArgumentNullException(nameof(services));
17+
18+
return services.AddFunctionsAuthentication(delegate { });
19+
}
1820

19-
public static AuthenticationBuilder AddAuthentication(this IFunctionsHostBuilder builder)
21+
/// <summary>
22+
/// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
23+
/// </summary>
24+
/// <param name="builder">The <see cref="IFunctionsHostBuilder"/> for the current application.</param>
25+
/// <returns>A <see cref="FunctionsAuthenticationBuilder"/> instance to configure authentication schemes.</returns>
26+
public static FunctionsAuthenticationBuilder AddAuthentication(this IFunctionsHostBuilder builder)
2027
{
21-
if (builder == null)
22-
{
23-
throw new ArgumentNullException(nameof(builder));
24-
}
28+
if (builder is null) throw new ArgumentNullException(nameof(builder));
2529

26-
return builder.AddAuthentication(null);
30+
return builder.Services.AddFunctionsAuthentication(delegate { });
2731
}
2832

29-
public static AuthenticationBuilder AddAuthentication(
33+
/// <summary>
34+
/// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
35+
/// </summary>
36+
/// <param name="builder">The <see cref="IFunctionsHostBuilder"/> for the current application.</param>
37+
/// <param name="configure">The <see cref="AuthenticationOptions"/> configuration logic.</param>
38+
/// <returns>A <see cref="FunctionsAuthenticationBuilder"/> instance to configure authentication schemes.</returns>
39+
/// <exception cref="ArgumentNullException">When builder is null.</exception>
40+
public static FunctionsAuthenticationBuilder AddAuthentication(
3041
this IFunctionsHostBuilder builder, Action<AuthenticationOptions>? configure)
3142
{
32-
if (builder == null)
43+
if (builder is null) throw new ArgumentNullException(nameof(builder));
44+
45+
return builder.Services.AddFunctionsAuthentication(configure);
46+
}
47+
48+
/// <summary>
49+
/// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
50+
/// </summary>
51+
/// <param name="configure">The <see cref="AuthenticationOptions"/> configuration logic.</param>
52+
public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
53+
this IServiceCollection services, Action<AuthenticationOptions>? configure)
54+
{
55+
var authBuilder = new FunctionsAuthenticationBuilder(services);
56+
57+
if (AuthHelper.EnableAuth)
3358
{
34-
throw new ArgumentNullException(nameof(builder));
59+
EnabledAuthHelper.AddBuiltInFunctionsAuthentication(services);
3560
}
36-
61+
else
62+
{
63+
services.AddAuthentication();
64+
AuthHelper.AddScriptJwtBearer(authBuilder);
65+
DisabledAuthHelper.AddScriptAuthLevel(authBuilder);
66+
DisabledAuthHelper.AddArmToken(authBuilder);
67+
}
68+
3769
if (configure != null)
3870
{
39-
builder.Services.AddSingleton<IConfigureOptions<AuthenticationOptions>>(provider =>
71+
services.AddSingleton<IConfigureOptions<AuthenticationOptions>>(provider =>
4072
new ConfigureOptions<AuthenticationOptions>(options =>
4173
{
4274
configure(options);
4375
}));
4476
}
4577

46-
return builder.Services
47-
.AddAuthentication()
48-
.AddScriptFunctionsJwtBearer();
49-
}
50-
51-
private static AuthenticationBuilder AddScriptFunctionsJwtBearer(this AuthenticationBuilder builder)
52-
{
53-
return builder.AddJwtBearer(Constants.WebJobsAuthScheme, options =>
54-
{
55-
options.Events = new JwtBearerEvents
56-
{
57-
OnMessageReceived = c =>
58-
{
59-
options.TokenValidationParameters = CreateTokenValidationParameters();
60-
return Task.CompletedTask;
61-
},
62-
63-
OnTokenValidated = c =>
64-
{
65-
c.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim(AuthLevelClaimType, AuthorizationLevel.Admin.ToString()) }));
66-
c.Success();
67-
return Task.CompletedTask;
68-
}
69-
};
70-
71-
options.TokenValidationParameters = CreateTokenValidationParameters();
72-
});
73-
74-
TokenValidationParameters CreateTokenValidationParameters()
75-
{
76-
var defaultKey = "2d3a0617-f369-492c-ab7a-f21ec1631376";
77-
var result = new TokenValidationParameters();
78-
79-
if (defaultKey != null)
80-
{
81-
result.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(defaultKey));
82-
result.ValidateAudience = true;
83-
result.ValidateIssuer = true;
84-
result.ValidAudience = string.Format("https://{0}.azurewebsites.net/azurefunctions", "func");
85-
result.ValidIssuer = string.Format("https://{0}.scm.azurewebsites.net", "func");
86-
}
87-
88-
return result;
89-
}
78+
return authBuilder;
9079
}
9180
}
9281
}

0 commit comments

Comments
 (0)