diff --git a/.assets/icon.png b/.assets/icon.png
new file mode 100644
index 0000000..0b9501b
Binary files /dev/null and b/.assets/icon.png differ
diff --git a/.build/release.props b/.build/release.props
index 091a0f4..f400a5e 100644
--- a/.build/release.props
+++ b/.build/release.props
@@ -1,35 +1,38 @@
-
- Arturo Martinez
- DarkLoop
- DarkLoop.Azure.Functions.Authorize
- false
- 3.0.0.0
- 3.1.3
- $(Version).0
- https://github.com/dark-loop/functions-authorize
- https://github.com/dark-loop/functions-authorize/blob/master/LICENSE
- Git
- AuthorizeAttribute, Authorize, Azure Functions, Azure, Bearer, JWT, Policy based authorization
- https://en.gravatar.com/userimage/22176525/45f25acea686a783e5b2ca172d72db71.png
- true
- dl-sftwr-sn-key.snk
- true
- Azure Functions V3 authentication extensions to enable authentication and authorization on a per function basis based on ASPNET Core frameworks.
- README.md
-
+
+ $(AssmeblyName)
+ Arturo Martinez
+ DarkLoop
+ DarkLoop - All rights reserved
+ DarkLoop's Azure Functions Authorization
+ true
+ 4.0.0.0
+ 4.0.0
+ $(Version).0
+ https://github.com/dark-loop/functions-authorize
+ https://github.com/dark-loop/functions-authorize/blob/master/LICENSE
+ Git
+ AuthorizeAttribute, Authorize, Azure Functions, Azure, Bearer, JWT, Policy based authorization
+ icons/icon.png
+ true
+ ../dl-sftwr-sn-key.snk
+ 0024000004800000940000000602000000240000525341310004000001000100791e7f618a12452d7ced5310f6203d0d227f9d26b146555e7e67a1801695dcf7c552421620a662f54b072f7be1efa885c074d4b9c76a4d6d154721d1c3b1f39164cfaf9ebdf9b7672ff320c89c5a64c90e25330f90a12bf42a1c57b70523e785167dbbfb7a0fdc9eb8d15112f758b89bab51953b08cfb2218095bc45171c99c5
+ true
+ README.md
+
-
- $(BuildNumber.Substring($([MSBuild]::Add($(BuildNumber.LastIndexOf('.')), 1))))
- $([System.DateTime]::Now.ToString('yyMMdd'))
- $(DateNumber)-$(Revision)
- -preview-$(BuildIDNumber)
- $(Version)$(PreviewVersion)
-
+
+ $(BuildNumber.Substring($([MSBuild]::Add($(BuildNumber.LastIndexOf('.')), 1))))
+ $([System.DateTime]::Now.ToString('yyMMdd'))
+ $(DateNumber)-$(Revision)
+ -preview-$(BuildIDNumber)
+ $(Version)$(PreviewVersion)
+
-
+
+
diff --git a/.editorconfig b/.editorconfig
index 5e51eda..736a2d3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,6 @@
[*.cs]
+file_header_template = \n Copyright (c) DarkLoop. All rights reserved.\n
+
# CS0618: Type or member is obsolete
dotnet_diagnostic.CS0618.severity = silent
diff --git a/Functions-Authorize.sln b/Functions-Authorize.sln
index 032d206..1ddf4c2 100644
--- a/Functions-Authorize.sln
+++ b/Functions-Authorize.sln
@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.32804.182
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D96FC724-6F6E-400E-BCA9-21A8FD44CA1C}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{051D16F8-54BB-482B-B2A9-47E2DA89E6DB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{1CED3FA2-C2EF-4A83-A9D6-B5193EF46B2C}"
@@ -12,12 +10,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{1CED3F
.build\release.props = .build\release.props
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorize", "src\DarkLoop.Azure.Functions.Authorize\DarkLoop.Azure.Functions.Authorize.csproj", "{47791FF8-5FEA-42BC-8192-5A59D23ADE92}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorize.Tests", "test\DarkLoop.Azure.Functions.Authorize.Tests\DarkLoop.Azure.Functions.Authorize.Tests.csproj", "{CB2B4A20-7882-4003-ADEF-E95F72DA3146}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{3A9A7517-71B4-429E-A91E-2DB7F695A363}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
.gitignore = .gitignore
LICENSE = LICENSE
README.md = README.md
@@ -25,15 +20,30 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{3A9A7517
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{53EC585B-CE9B-4E7D-B2E2-F7A9B6DA0FE7}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorize.SampleFunctions", "sample\DarkLoop.Azure.Functions.Authorize.SampleFunctions\DarkLoop.Azure.Functions.Authorize.SampleFunctions.csproj", "{9AB1B297-FA02-406C-A3E2-979A7CC5C706}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C3D01C4-AFF0-4AE3-ACA1-FDCDF8FD6CE1}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
README.md = README.md
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Darkloop.Azure.Functions.Authorize.SampleFunctions.V4", "sample\Darkloop.Azure.Functions.Authorize.SampleFunctions.V4\Darkloop.Azure.Functions.Authorize.SampleFunctions.V4.csproj", "{0E97CAE1-D5E8-462D-B513-7EF2797C7D48}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleInProcFunctions.V4", "sample\SampleInProcFunctions.V4\SampleInProcFunctions.V4.csproj", "{0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleIsolatedFunctions.V4", "sample\SampleIsolatedFunctions.V4\SampleIsolatedFunctions.V4.csproj", "{004B35A8-CD74-4E05-B601-9AFFD31427FF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Abstractions.Tests", "test\Abstractions.Tests\Abstractions.Tests.csproj", "{D29BBE67-EE8E-4085-A004-F12E68FD7FB6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{284270FA-3689-46E6-B74C-249EA8981B78}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorization.Abstractions", "src\abstractions\DarkLoop.Azure.Functions.Authorization.Abstractions.csproj", "{323DA8F9-DC74-491D-9B52-8F58ABAB2B47}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorize", "src\in-proc\DarkLoop.Azure.Functions.Authorize.csproj", "{5766D189-64CA-4735-8378-5D5B529F8EB1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkLoop.Azure.Functions.Authorization.Isolated", "src\isolated\DarkLoop.Azure.Functions.Authorization.Isolated.csproj", "{05468E2C-BFCF-49A4-A083-8BEE26767FA5}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".assets", ".assets", "{2A6C3077-8AC9-49AA-919E-88B0BF1BFECA}"
+ ProjectSection(SolutionItems) = preProject
+ .assets\icon.png = .assets\icon.png
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -41,31 +51,41 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {47791FF8-5FEA-42BC-8192-5A59D23ADE92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {47791FF8-5FEA-42BC-8192-5A59D23ADE92}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {47791FF8-5FEA-42BC-8192-5A59D23ADE92}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {47791FF8-5FEA-42BC-8192-5A59D23ADE92}.Release|Any CPU.Build.0 = Release|Any CPU
- {CB2B4A20-7882-4003-ADEF-E95F72DA3146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CB2B4A20-7882-4003-ADEF-E95F72DA3146}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CB2B4A20-7882-4003-ADEF-E95F72DA3146}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CB2B4A20-7882-4003-ADEF-E95F72DA3146}.Release|Any CPU.Build.0 = Release|Any CPU
- {9AB1B297-FA02-406C-A3E2-979A7CC5C706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AB1B297-FA02-406C-A3E2-979A7CC5C706}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9AB1B297-FA02-406C-A3E2-979A7CC5C706}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9AB1B297-FA02-406C-A3E2-979A7CC5C706}.Release|Any CPU.Build.0 = Release|Any CPU
- {0E97CAE1-D5E8-462D-B513-7EF2797C7D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0E97CAE1-D5E8-462D-B513-7EF2797C7D48}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0E97CAE1-D5E8-462D-B513-7EF2797C7D48}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0E97CAE1-D5E8-462D-B513-7EF2797C7D48}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43}.Release|Any CPU.Build.0 = Release|Any CPU
+ {004B35A8-CD74-4E05-B601-9AFFD31427FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {004B35A8-CD74-4E05-B601-9AFFD31427FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {004B35A8-CD74-4E05-B601-9AFFD31427FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {004B35A8-CD74-4E05-B601-9AFFD31427FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D29BBE67-EE8E-4085-A004-F12E68FD7FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D29BBE67-EE8E-4085-A004-F12E68FD7FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D29BBE67-EE8E-4085-A004-F12E68FD7FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D29BBE67-EE8E-4085-A004-F12E68FD7FB6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {323DA8F9-DC74-491D-9B52-8F58ABAB2B47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {323DA8F9-DC74-491D-9B52-8F58ABAB2B47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {323DA8F9-DC74-491D-9B52-8F58ABAB2B47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {323DA8F9-DC74-491D-9B52-8F58ABAB2B47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5766D189-64CA-4735-8378-5D5B529F8EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5766D189-64CA-4735-8378-5D5B529F8EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5766D189-64CA-4735-8378-5D5B529F8EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5766D189-64CA-4735-8378-5D5B529F8EB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05468E2C-BFCF-49A4-A083-8BEE26767FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05468E2C-BFCF-49A4-A083-8BEE26767FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05468E2C-BFCF-49A4-A083-8BEE26767FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05468E2C-BFCF-49A4-A083-8BEE26767FA5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {47791FF8-5FEA-42BC-8192-5A59D23ADE92} = {D96FC724-6F6E-400E-BCA9-21A8FD44CA1C}
- {CB2B4A20-7882-4003-ADEF-E95F72DA3146} = {051D16F8-54BB-482B-B2A9-47E2DA89E6DB}
- {9AB1B297-FA02-406C-A3E2-979A7CC5C706} = {53EC585B-CE9B-4E7D-B2E2-F7A9B6DA0FE7}
- {0E97CAE1-D5E8-462D-B513-7EF2797C7D48} = {53EC585B-CE9B-4E7D-B2E2-F7A9B6DA0FE7}
+ {0C7F5C24-C2B2-46DC-8DB7-B58BED3DDE43} = {53EC585B-CE9B-4E7D-B2E2-F7A9B6DA0FE7}
+ {004B35A8-CD74-4E05-B601-9AFFD31427FF} = {53EC585B-CE9B-4E7D-B2E2-F7A9B6DA0FE7}
+ {D29BBE67-EE8E-4085-A004-F12E68FD7FB6} = {051D16F8-54BB-482B-B2A9-47E2DA89E6DB}
+ {323DA8F9-DC74-491D-9B52-8F58ABAB2B47} = {284270FA-3689-46E6-B74C-249EA8981B78}
+ {5766D189-64CA-4735-8378-5D5B529F8EB1} = {284270FA-3689-46E6-B74C-249EA8981B78}
+ {05468E2C-BFCF-49A4-A083-8BEE26767FA5} = {284270FA-3689-46E6-B74C-249EA8981B78}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {546E3A6C-060C-4630-BEEE-46A1F8715347}
diff --git a/README.md b/README.md
index 8ebb630..9f88042 100644
--- a/README.md
+++ b/README.md
@@ -1,89 +1,16 @@
# functions-authorize
-Bringing AuthorizeAttribute Behavior to Azure Functions v3 and v4 (In-Process)
+Extension bringing AuthorizeAttribute Behavior to Azure Functions In-Proc and Isolated mode. For the latter is only available with ASPNET Core integration.
It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does.
+## Getting Started
+- [Azure Functions V3+ In-Proc mode](./in-proc/README.md)
+- [Azure Functions V4 Isolated mode with ASPNET Core integration](./isolated/README.md)
+
## License
This projects is open source and may be redistributed under the terms of the [Apache 2.0](http://opensource.org/licenses/Apache-2.0) license.
-## Using the package
-### Installing the package
-`dotnet add package DarkLoop.Azure.Functions.Authorize`
-
-### Setting up authentication
-The goal is to utilize the same authentication framework provided for ASP.NET Core
-```c#
-using Microsoft.Azure.Functions.Extensions.DependencyInjection;
-using MyFunctionAppNamespace;
-
-[assembly: FunctionsStartup(typeof(Startup))]
-namespace MyFunctionAppNamespace
-{
- class Startup : FunctionsStartup
- {
- public void Configure(IFunctionsHostBuilder builder)
- {
- builder
- .AddAuthentication(options =>
- {
- options.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme;
- options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- })
- .AddOpenIdConnect(options =>
- {
- options.ClientId = "";
- // ... more options here
- })
- .AddJwtBearer(options =>
- {
- options.Audience = "";
- // ... more options here
- });
-
- builder
- .AddAuthorization(options =>
- {
- options.AddPolicy("OnlyAdmins", policyBuilder =>
- {
- // configure my policy requirements
- });
- });
- }
- }
-}
-```
-
-No need to register the middleware the way we do for ASP.NET Core applications.
-
-### Using the attribute
-And now lets use `FunctionAuthorizeAttribute` the same way we use `AuthorizeAttribute` in our ASP.NET Core applications.
-```C#
-public class Functions
-{
- [FunctionAuthorize]
- [FunctionName("get-record")]
- public async Task GetRecord(
- [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
- ILogger log)
- {
- var user = req.HttpContext.User;
- var record = GetUserData(user.Identity.Name);
- return new OkObjectResult(record);
- }
-
- [FunctionAuthorize(Policy = "OnlyAdmins")]
- [FunctionName("get-all-records")]
- public async Task(
- [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
- ILogger log)
- {
- var records = GetAllData();
- return new OkObjectResult(records);
- }
-}
-```
-##
-
+## Package Status
### Releases
[](https://www.nuget.org/packages/DarkLoop.Azure.Functions.Authorize)
@@ -93,32 +20,36 @@ public class Functions
## Change log
Adding change log starting with version 3.1.3
+### 4.0.0
+Information will be added soon.
+
### 3.1.3
+3.1.3 and lower versions only support Azure Functions V3 In-Proc mode. Starting from 4.0.0, support for Azure Functions V4 Isolated mode with ASPNET Core integration is added.
- #### Support for disabling `FunctionAuthorize` effect at the application level.
Adding support for disabling the effect of `[FunctionAuthorize]` attribute at the application level.
This is useful when wanting to disable authorization for a specific environment, such as local development.
When configuring services, you can now configure `FunctionsAuthorizationOptions`.
- ```c#
+ ```csharp
builder.Services.Configure(options =>
options.DisableAuthorization = Configuration.GetValue("AuthOptions:DisableAuthorization"));
```
Optionally you can bind it to configuration to rely on providers like User Secrets or Azure App Configuration to disable and re-enable without having to restart your application:
- ```c#
+ ```csharp
builder.Services.Configure(
Configuration.GetSection("FunctionsAuthorization"));
```
For function apps targeting .NET 7 or greater, you can also use `AuthorizationBuilder` to set this value:
- ```c#
+ ```csharp
builder.Services
.AddAuthorizationBuilder()
.DisableAuthorization(Configuration.GetValue("AuthOptions:DisableAuthorization"));
```
It's always recommended to encapsulate this logic within checks for environments to ensure that if the configuration setting is unintentionally moved to a non-desired environment, it would not affect security of our HTTP triggered functions. This change adds a helper method to identify if you are running the function app in the local environment:
- ```c#
+ ```csharp
if (builder.IsLocalAuthorizationContext())
{
builder.Services.Configure(
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/.gitignore b/sample/SampleInProcFunctions.V4/.gitignore
similarity index 100%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/.gitignore
rename to sample/SampleInProcFunctions.V4/.gitignore
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Function1.cs b/sample/SampleInProcFunctions.V4/Function1.cs
similarity index 92%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Function1.cs
rename to sample/SampleInProcFunctions.V4/Function1.cs
index 485d06f..df122fc 100644
--- a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Function1.cs
+++ b/sample/SampleInProcFunctions.V4/Function1.cs
@@ -12,8 +12,9 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Text;
+using Microsoft.AspNetCore.Authorization;
-namespace Darkloop.Azure.Functions.Authorize.SampleFunctions.V4
+namespace DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4
{
public static class TestFunction
{
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Properties/serviceDependencies.json b/sample/SampleInProcFunctions.V4/Properties/serviceDependencies.json
similarity index 100%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Properties/serviceDependencies.json
rename to sample/SampleInProcFunctions.V4/Properties/serviceDependencies.json
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Properties/serviceDependencies.local.json b/sample/SampleInProcFunctions.V4/Properties/serviceDependencies.local.json
similarity index 100%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Properties/serviceDependencies.local.json
rename to sample/SampleInProcFunctions.V4/Properties/serviceDependencies.local.json
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4.csproj b/sample/SampleInProcFunctions.V4/SampleInProcFunctions.V4.csproj
similarity index 72%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4.csproj
rename to sample/SampleInProcFunctions.V4/SampleInProcFunctions.V4.csproj
index 87dbdd4..0160c2e 100644
--- a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4.csproj
+++ b/sample/SampleInProcFunctions.V4/SampleInProcFunctions.V4.csproj
@@ -1,16 +1,17 @@
-
+
net6.0
v4
51dc0b9d-8e74-45ec-aebc-1d3d6934faf5
-
+ false
+
-
+
-
+
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Startup.cs b/sample/SampleInProcFunctions.V4/Startup.cs
similarity index 83%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Startup.cs
rename to sample/SampleInProcFunctions.V4/Startup.cs
index 9d09eac..df444ca 100644
--- a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/Startup.cs
+++ b/sample/SampleInProcFunctions.V4/Startup.cs
@@ -1,15 +1,14 @@
-using DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4;
-using DarkLoop.Azure.Functions.Authorize.Security;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4;
using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.Text;
[assembly: FunctionsStartup(typeof(Startup))]
@@ -39,21 +38,21 @@ public override void Configure(IFunctionsHostBuilder builder)
{
var body = "Unauthorized request";
var response = x.Response;
+ response.StatusCode = 401;
response.ContentType = "text/plain";
response.ContentLength = body.Length;
- response.StatusCode = 401;
await response.WriteAsync(body);
await response.Body.FlushAsync();
},
OnChallenge = async x =>
{
// un-commenting the following lines would override what the internals do to send an unauthorized response
- //var response = x.Response;
- //response.ContentType = "text/plain";
- //response.ContentLength = 5;
- //response.StatusCode = 401;
- //await response.WriteAsync("No go");
- //await response.Body.FlushAsync();
+ var response = x.Response;
+ response.StatusCode = 401;
+ response.ContentType = "text/plain";
+ response.ContentLength = 5;
+ await response.WriteAsync("No go");
+ await response.Body.FlushAsync();
}
};
}, true);
diff --git a/sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/host.json b/sample/SampleInProcFunctions.V4/host.json
similarity index 100%
rename from sample/Darkloop.Azure.Functions.Authorize.SampleFunctions.V4/host.json
rename to sample/SampleInProcFunctions.V4/host.json
diff --git a/src/DarkLoop.Azure.Functions.Authorize/.gitignore b/sample/SampleIsolatedFunctions.V4/.gitignore
similarity index 100%
rename from src/DarkLoop.Azure.Functions.Authorize/.gitignore
rename to sample/SampleIsolatedFunctions.V4/.gitignore
diff --git a/sample/SampleIsolatedFunctions.V4/Function1.cs b/sample/SampleIsolatedFunctions.V4/Function1.cs
new file mode 100644
index 0000000..0edaf5e
--- /dev/null
+++ b/sample/SampleIsolatedFunctions.V4/Function1.cs
@@ -0,0 +1,47 @@
+using DarkLoop.Azure.Functions.Authorization;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System.Reflection;
+using System.Text;
+
+namespace SampleIsolatedFunctions.V4
+{
+ [FunctionAuthorize]
+ public class Function1
+ {
+ private readonly ILogger _logger;
+
+ public Function1(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Function("Function1")]
+ public async Task Run([HttpTrigger("get", "post")] HttpRequest req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var provider = req.HttpContext.RequestServices;
+ var schProvider = provider.GetService();
+
+ var sb = new StringBuilder();
+
+ if (schProvider is not null)
+ {
+ foreach (var scheme in await schProvider.GetAllSchemesAsync())
+ sb.AppendLine($"{scheme.Name} -> {scheme.HandlerType}");
+ }
+
+ sb.AppendLine();
+ sb.AppendLine(Assembly.GetEntryAssembly()!.FullName);
+
+ return new OkObjectResult(sb.ToString());
+
+ }
+ }
+}
diff --git a/sample/SampleIsolatedFunctions.V4/Program.cs b/sample/SampleIsolatedFunctions.V4/Program.cs
new file mode 100644
index 0000000..bd406b7
--- /dev/null
+++ b/sample/SampleIsolatedFunctions.V4/Program.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWebApplication(builder =>
+ {
+ builder.UseFunctionsAuthorization();
+ })
+ .ConfigureServices(services =>
+ {
+ services
+ .AddFunctionsAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ });
+
+ services.AddFunctionsAuthorization();
+
+ services.AddApplicationInsightsTelemetryWorkerService();
+ services.ConfigureFunctionsApplicationInsights();
+ })
+ .Build();
+
+host.Run();
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Properties/serviceDependencies.json b/sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.json
similarity index 100%
rename from src/DarkLoop.Azure.Functions.Authorize/Properties/serviceDependencies.json
rename to sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.json
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Properties/serviceDependencies.local.json b/sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.local.json
similarity index 100%
rename from src/DarkLoop.Azure.Functions.Authorize/Properties/serviceDependencies.local.json
rename to sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.local.json
diff --git a/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj b/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj
new file mode 100644
index 0000000..c31d14a
--- /dev/null
+++ b/sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj
@@ -0,0 +1,36 @@
+
+
+ net6.0
+ v4
+ Exe
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/SampleIsolatedFunctions.V4/host.json b/sample/SampleIsolatedFunctions.V4/host.json
new file mode 100644
index 0000000..ee5cf5f
--- /dev/null
+++ b/sample/SampleIsolatedFunctions.V4/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Bindings/FunctionsAuthorizeBindingProvider.cs b/src/DarkLoop.Azure.Functions.Authorize/Bindings/FunctionsAuthorizeBindingProvider.cs
deleted file mode 100644
index 8e02d19..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Bindings/FunctionsAuthorizeBindingProvider.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.Linq;
-using System.Net.Http;
-using System.Reflection;
-using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Azure.WebJobs;
-using Microsoft.Azure.WebJobs.Host.Bindings;
-using Microsoft.Extensions.Logging;
-
-namespace DarkLoop.Azure.Functions.Authorize.Bindings
-{
- internal class FunctionsAuthorizeBindingProvider : IBindingProvider
- {
- private readonly IFunctionsAuthorizationFilterIndex _filtersIndex;
-
- public FunctionsAuthorizeBindingProvider(IFunctionsAuthorizationFilterIndex filterIndex)
- {
- _filtersIndex = filterIndex;
- }
-
- public async Task TryCreateAsync(BindingProviderContext context)
- {
- if (context == null) throw new ArgumentNullException(nameof(context));
-
- var paramType = context.Parameter.ParameterType;
- if (paramType == typeof(HttpRequest) || paramType == typeof(HttpRequestMessage))
- {
- await this.ProcessAuthorizationAsync(context.Parameter);
- }
-
- return null;
- }
-
- private Task ProcessAuthorizationAsync(ParameterInfo info)
- {
- var method = info.Member as MethodInfo;
-
- if (method == null) throw new InvalidOperationException($"Unable to bind authorization context for {info.Name}.");
-
- var cls = method.DeclaringType;
-
- var allowAnonymous = method.GetCustomAttribute();
-
- if (allowAnonymous != null) return Task.CompletedTask;
-
- var classAuthAttrs = cls.GetCustomAttributes();
- var methodAuthAttrs = method.GetCustomAttributes();
- var nameAttr = method.GetCustomAttribute();
-
- var allAuthAttributes = classAuthAttrs.Concat(methodAuthAttrs).ToList();
-
- if (allAuthAttributes.Count > 0)
- {
- _filtersIndex.AddAuthorizationFilter(method, nameAttr, allAuthAttributes);
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Constants.cs b/src/DarkLoop.Azure.Functions.Authorize/Constants.cs
deleted file mode 100644
index 10799af..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Constants.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace DarkLoop.Azure.Functions.Authorize
-{
- internal class Constants
- {
- internal const string AuthInvokedKey = "__WebJobAuthInvoked";
- internal const string WebJobsAuthScheme = "WebJobsAuthLevel";
- internal const string ArmTokenAuthScheme = "ArmToken";
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/DarkLoop.Azure.Functions.Authorize.csproj b/src/DarkLoop.Azure.Functions.Authorize/DarkLoop.Azure.Functions.Authorize.csproj
deleted file mode 100644
index 8328818..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/DarkLoop.Azure.Functions.Authorize.csproj
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- netstandard2.1;net7.0
- 0.0.1-preview
- DarkLoop
- DarkLoop - All rights reserved
- Azure Functions Authorize
- Allows same AuthorizeAttribute behavior for Azure Functions V3+
- v3
- enable
- 3472ff41-3859-4101-a2da-6c37d751fd31
-
-
-
- TRACE
-
-
-
- DEBUG;TRACE
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
-
-
-
-
-
-
-
-
-
-
- true
-
-
-
-
-
- PreserveNewest
- Never
-
-
-
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionAuthorizationFilterIndex.cs b/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionAuthorizationFilterIndex.cs
deleted file mode 100644
index 4f4a9dc..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionAuthorizationFilterIndex.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Reflection;
-using DarkLoop.Azure.Functions.Authorize.Security;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.Azure.WebJobs;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-
-namespace DarkLoop.Azure.Functions.Authorize.Filters
-{
- class FunctionAuthorizationFilterIndex : IFunctionsAuthorizationFilterIndex
- {
- private readonly ConcurrentDictionary _index =
- new ConcurrentDictionary();
-
- private readonly IServiceProvider _serviceProvider;
- private readonly ObjectFactory _filterFactory;
-
- public FunctionAuthorizationFilterIndex(
- IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- _filterFactory = ActivatorUtilities.CreateFactory(typeof(FunctionsAuthorizeFilter), new[] { typeof(IEnumerable) });
- }
-
- public void AddAuthorizationFilter(MethodInfo functionMethod, FunctionNameAttribute nameAttribute, IEnumerable authorizeData)
- {
- if (functionMethod is null) throw new ArgumentNullException(nameof(functionMethod));
- if (authorizeData is null) throw new ArgumentNullException(nameof(authorizeData));
-
- var name = this.GetFunctionName(functionMethod, nameAttribute);
- var filter = _filterFactory.Invoke(_serviceProvider, new[] { authorizeData }) as FunctionsAuthorizeFilter;
-
- if (!this._index.TryAdd(name, filter!))
- {
- throw new InvalidOperationException($"An authorization filter for function {name} has already been processed. Make sure function names are unique within your Functions App.");
- }
- }
-
- public IFunctionsAuthorizeFilter? GetAuthorizationFilter(string functionName)
- {
- _index.TryGetValue(functionName, out var stored);
-
- return stored;
- }
-
- private string GetFunctionName(MethodInfo method, FunctionNameAttribute nameAttribute)
- {
- if (nameAttribute is null)
- {
- return $"{method.DeclaringType!.Name}.{method.Name}";
- }
- else
- {
- return nameAttribute.Name;
- }
- }
-
-
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionsAuthorizeFilter.cs b/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionsAuthorizeFilter.cs
deleted file mode 100644
index 8d6ca8a..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Filters/FunctionsAuthorizeFilter.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Security;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Authorization.Policy;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-
-namespace DarkLoop.Azure.Functions.Authorize.Filters
-{
- internal class FunctionsAuthorizeFilter : IFunctionsAuthorizeFilter
- {
- private static readonly IEnumerable __dismissedSchemes =
- AuthHelper.EnableAuth ?
- new[] { Constants.WebJobsAuthScheme } :
- new[] { Constants.WebJobsAuthScheme, Constants.ArmTokenAuthScheme };
-
- private readonly IOptionsMonitor _authOptionsMonitor;
- private readonly ILogger _logger;
-
- public IEnumerable AuthorizeData { get; }
-
- public IAuthenticationSchemeProvider SchemeProvider { get; }
-
- public IAuthorizationPolicyProvider PolicyProvider { get; }
-
- public AuthorizationPolicy Policy { get; }
-
- public FunctionsAuthorizeFilter(
- IAuthenticationSchemeProvider schemeProvider,
- IAuthorizationPolicyProvider policyProvider,
- IEnumerable authorizeData,
- IOptionsMonitor authorizationOptions,
- ILogger logger)
- {
- this._authOptionsMonitor = authorizationOptions;
- this._logger = logger;
- this.SchemeProvider = schemeProvider;
- this.PolicyProvider = policyProvider;
- this.AuthorizeData = authorizeData;
-
- this.IntegrateSchemes();
- this.Policy = this.ComputePolicyAsync().GetAwaiter().GetResult();
- }
-
- private void IntegrateSchemes()
- {
- var schemes = this.SchemeProvider.GetAllSchemesAsync().GetAwaiter().GetResult();
- var strSchemes = string.Join(',',
- from scheme in schemes
- where !__dismissedSchemes.Contains(scheme.Name)
- select scheme.Name);
-
- foreach (var data in this.AuthorizeData)
- {
- // only setting auth schemes if they have not been specified already
- if (string.IsNullOrWhiteSpace(data.AuthenticationSchemes))
- {
- data.AuthenticationSchemes = strSchemes;
- }
- }
- }
-
- public async Task AuthorizeAsync(FunctionAuthorizationContext context)
- {
- if (this._authOptionsMonitor.CurrentValue.AuthorizationDisabled)
- {
- _logger.LogWarning(
- $"Authorization through FunctionAuthorizeAttribute is disabled at the application level. Skipping authorization for {context.HttpContext.Request.GetDisplayUrl()}.");
-
- return;
- }
-
- if (context is null) throw new ArgumentNullException(nameof(context));
-
- if (context.HttpContext.Items.ContainsKey(Constants.AuthInvokedKey))
- {
- return;
- }
-
- var httpContext = context.HttpContext;
- var evaluator = httpContext.RequestServices.GetRequiredService();
- var authenticateResult = await evaluator.AuthenticateAsync(this.Policy, httpContext);
- var authorizeResult = await evaluator.AuthorizeAsync(this.Policy, authenticateResult, httpContext, context);
-
- if (authorizeResult.Challenged)
- {
- context.Result = new ChallengeResult(this.Policy.AuthenticationSchemes.ToArray());
- }
- else if (authorizeResult.Forbidden)
- {
- context.Result = new ForbidResult(this.Policy.AuthenticationSchemes.ToArray());
- }
- else if (!authorizeResult.Succeeded)
- {
- context.Result = new UnauthorizedResult();
- }
- }
-
- private Task ComputePolicyAsync()
- {
- if (this.PolicyProvider == null)
- {
- throw new InvalidOperationException("Policy cannot be created.");
- }
-
- return AuthorizationPolicy.CombineAsync(this.PolicyProvider, this.AuthorizeData)!;
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizationFilterIndex.cs b/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizationFilterIndex.cs
deleted file mode 100644
index f391cc4..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizationFilterIndex.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using System.Reflection;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.Azure.WebJobs;
-
-namespace DarkLoop.Azure.Functions.Authorize.Filters
-{
- interface IFunctionsAuthorizationFilterIndex
- {
- IFunctionsAuthorizeFilter? GetAuthorizationFilter(string functionName);
-
- void AddAuthorizationFilter(MethodInfo functionMethod, FunctionNameAttribute nameAttribute, IEnumerable authorizeData);
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizeFilter.cs b/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizeFilter.cs
deleted file mode 100644
index 36c0e80..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Filters/IFunctionsAuthorizeFilter.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Security;
-
-namespace DarkLoop.Azure.Functions.Authorize.Filters
-{
- interface IFunctionsAuthorizeFilter
- {
- Task AuthorizeAsync(FunctionAuthorizationContext context);
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthExtension.cs b/src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthExtension.cs
deleted file mode 100644
index b4797c9..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthExtension.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Microsoft.Azure.WebJobs.Description;
-using Microsoft.Azure.WebJobs.Host.Config;
-
-namespace DarkLoop.Azure.Functions.Authorize
-{
- [Extension("FunctionsAuthorize")]
- class FunctionsAuthExtension : IExtensionConfigProvider
- {
- public void Initialize(ExtensionConfigContext context)
- {
-
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Handlers/AuthenticationRequestHandler.cs b/src/DarkLoop.Azure.Functions.Authorize/Handlers/AuthenticationRequestHandler.cs
deleted file mode 100644
index 0a57e59..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Handlers/AuthenticationRequestHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace DarkLoop.Azure.Functions.Authorize.Handlers
-{
- public static class AuthenticationRequestHandler
- {
- public static async Task HandleAuthenticateAsync(HttpContext context, string scheme)
- {
- var handler = await GetSchemeHandler(context, scheme);
- if(await handler.HandleRequestAsync())
- {
- return null;
- }
-
- return new UnauthorizedResult();
- }
-
-
- private static async Task GetSchemeHandler(HttpContext context, string scheme)
- {
- var handlers = context.RequestServices.GetRequiredService();
- var handler = await handlers.GetHandlerAsync(context, scheme) as IAuthenticationRequestHandler;
-
- if (handler != null)
- {
- return handler;
- }
- else
- {
- var schemes = context.RequestServices.GetRequiredService();
- var reqSchemes = await schemes.GetRequestHandlerSchemesAsync();
-
- throw new InvalidOperationException($"Scheme '{scheme}' is not a valid request authentication handler. List of valid available handlers: {string.Join(", ", reqSchemes.Select(x => x.Name))}");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthHelper.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/AuthHelper.cs
deleted file mode 100644
index cafd9a5..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthHelper.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using System.Linq.Expressions;
-using System.Reflection;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal class AuthHelper
- {
- protected static Assembly ScriptWebHostAssembly = Assembly.Load("Microsoft.Azure.WebJobs.Script.WebHost");
- protected static Type WebHostSvcCollectionExtType =
- ScriptWebHostAssembly.GetType("Microsoft.Azure.WebJobs.Script.WebHost.WebHostServiceCollectionExtensions");
- private static Type JwtSecurityExtsType =
- ScriptWebHostAssembly.GetType("Microsoft.Extensions.DependencyInjection.ScriptJwtBearerExtensions");
- private static Func __func = BuildFunc();
- private static Func __authorizationFunc = BuildAuthorizationFunc();
-
- internal static bool EnableAuth { get; private set; }
-
- internal static bool IsLocalDevelopment => !EnableAuth;
-
- static AuthHelper()
- {
- var entry = Assembly.GetEntryAssembly();
- var fullName = entry.FullName;
- var name = fullName.Substring(0, fullName.IndexOf(','));
- EnableAuth = name.Equals("Microsoft.Azure.WebJobs.Script.WebHost", StringComparison.OrdinalIgnoreCase);
- }
-
- private static Func BuildFunc()
- {
- var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
- var method = Expression.Call(JwtSecurityExtsType, "AddScriptJwtBearer", Type.EmptyTypes, builder);
- var lambda = Expression.Lambda>(method, builder);
-
- return lambda.Compile();
- }
-
- private static Func BuildAuthorizationFunc()
- {
- var services = Expression.Parameter(typeof(IServiceCollection), "services");
- var method = Expression.Call(WebHostSvcCollectionExtType, "AddWebJobsScriptHostAuthorization", Type.EmptyTypes, services);
- var lambda = Expression.Lambda>(method, services);
-
- return lambda.Compile();
- }
-
- internal static IServiceCollection AddFunctionsBuiltInAuthorization(IServiceCollection services)
- {
- return __authorizationFunc(services);
- }
-
- internal static FunctionsAuthenticationBuilder AddScriptJwtBearer(FunctionsAuthenticationBuilder builder)
- {
- __func(builder);
- return builder;
- }
- }
-}
\ No newline at end of file
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthenticationExtensions.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/AuthenticationExtensions.cs
deleted file mode 100644
index 9a72ed0..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthenticationExtensions.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using DarkLoop.Azure.Functions.Authorize.Security;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.Azure.Functions.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-
-namespace Microsoft.Extensions.DependencyInjection
-{
- public static class AuthenticationExtensions
- {
- ///
- /// Adds Functions built-in authentication.
- ///
- public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(this IServiceCollection services)
- {
- if (services is null) throw new ArgumentNullException(nameof(services));
-
- return services.AddFunctionsAuthentication(delegate { });
- }
-
- ///
- /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
- ///
- /// The for the current application.
- /// A instance to configure authentication schemes.
- public static FunctionsAuthenticationBuilder AddAuthentication(this IFunctionsHostBuilder builder)
- {
- if (builder is null) throw new ArgumentNullException(nameof(builder));
-
- return builder.Services.AddFunctionsAuthentication(delegate { });
- }
-
- ///
- /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
- ///
- /// The for the current application.
- /// The configuration logic.
- /// A instance to configure authentication schemes.
- /// When builder is null.
- public static FunctionsAuthenticationBuilder AddAuthentication(
- this IFunctionsHostBuilder builder, Action? configure)
- {
- if (builder is null) throw new ArgumentNullException(nameof(builder));
-
- return builder.Services.AddFunctionsAuthentication(configure);
- }
-
- ///
- /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
- ///
- /// The configuration logic.
- public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
- this IServiceCollection services, Action? configure)
- {
- var authBuilder = new FunctionsAuthenticationBuilder(services);
-
- if (AuthHelper.EnableAuth)
- {
- EnabledAuthHelper.AddBuiltInFunctionsAuthentication(services);
- }
- else
- {
- services.AddAuthentication();
- AuthHelper.AddScriptJwtBearer(authBuilder);
- DisabledAuthHelper.AddScriptAuthLevel(authBuilder);
- DisabledAuthHelper.AddArmToken(authBuilder);
- }
-
- if (configure != null)
- {
- services.AddSingleton>(provider =>
- new ConfigureOptions(options =>
- {
- configure(options);
- }));
- }
-
- return authBuilder;
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationExtensions.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationExtensions.cs
deleted file mode 100644
index 8e2856d..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationExtensions.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using DarkLoop.Azure.Functions.Authorize.Bindings;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using DarkLoop.Azure.Functions.Authorize.Security;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.Azure.Functions.Extensions.DependencyInjection;
-using Microsoft.Azure.WebJobs;
-using Microsoft.Azure.WebJobs.Host.Bindings;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Microsoft.Extensions.DependencyInjection
-{
- public static class AuthorizationExtensions
- {
- ///
- /// Adds Functions built-in authorization.
- ///
- /// The containing the services collection to configure.
- public static IFunctionsHostBuilder AddAuthorization(this IFunctionsHostBuilder builder)
- {
- if (builder == null) throw new ArgumentNullException(nameof(builder));
-
- builder.Services.AddFunctionsAuthorization(delegate { });
-
- return builder;
- }
-
- ///
- /// Adds Functions built-in authorization.
- ///
- /// The service collection to configure.
- public static IServiceCollection AddFunctionsAuthorization(this IServiceCollection services)
- {
- if (services is null) throw new ArgumentNullException(nameof(services));
-
- return services.AddFunctionsAuthorization(delegate { });
- }
-
- ///
- /// Adds Functions built-in authorization handlers and allows for further configuration.
- ///
- /// The containing the services collection to configure.
- /// The method to configure the authorization options.
- public static IFunctionsHostBuilder AddAuthorization(this IFunctionsHostBuilder builder, Action configure)
- {
- if (builder is null) throw new ArgumentNullException(nameof(builder));
-
- builder.Services.AddFunctionsAuthorization(configure);
-
- return builder;
- }
-
- ///
- /// Adds Function built-in authorization handlers and allows for further configuration.
- ///
- /// The service collection to configure.
- /// The method to configure the authorization options.
- public static IServiceCollection AddFunctionsAuthorization(
- this IServiceCollection services, Action configure)
- {
- if (services is null) throw new ArgumentNullException(nameof(services));
- if (configure is null) throw new ArgumentNullException(nameof(configure));
-
- AuthHelper.AddFunctionsBuiltInAuthorization(services);
- services.Configure(configure);
-
- return services;
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/DisabledAuthHelper.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/DisabledAuthHelper.cs
deleted file mode 100644
index c3eeabd..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/DisabledAuthHelper.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Microsoft.AspNetCore.Authentication;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Text;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal class DisabledAuthHelper : AuthHelper
- {
- private static Assembly FuncAssembly = Assembly.Load("func");
- private static MethodInfo AddSchemeMethod =
- typeof(AuthenticationBuilder).GetMethods().SingleOrDefault(x =>
- x.Name == "AddScheme" &&
- x.IsGenericMethod &&
- x.GetParameters().Length == 2);
- private static Type AuthLOptions =
- ScriptWebHostAssembly.GetType("Microsoft.Azure.WebJobs.Script.WebHost.Authentication.AuthenticationLevelOptions");
- private static Type ArmTOptions =
- ScriptWebHostAssembly.GetType("Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.ArmAuthenticationOptions");
- private static Type CliAuthHandler =
- FuncAssembly.GetType("Azure.Functions.Cli.Actions.HostActions.WebHost.Security.CliAuthenticationHandler`1");
-
- private static Func __authLDisabled = BuildAuthLFunc();
- private static Func __armTokenDisabled = BuildArmTFunc();
-
- private static Func BuildAuthLFunc()
- {
- var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
- var scheme = Expression.Constant(Constants.WebJobsAuthScheme);
- var action = Expression.Lambda(Expression.Empty(), Expression.Parameter(AuthLOptions, "options"));
- var genMethod = AddSchemeMethod.MakeGenericMethod(AuthLOptions, CliAuthHandler.MakeGenericType(AuthLOptions));
- var method = Expression.Call(builder, genMethod, scheme, action);
- var lambda = Expression.Lambda>(method, builder);
- return lambda.Compile();
- }
-
- private static Func BuildArmTFunc()
- {
- var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
- var scheme = Expression.Constant(Constants.ArmTokenAuthScheme);
- var action = Expression.Lambda(Expression.Empty(), Expression.Parameter(ArmTOptions, "options"));
- var genMethod = AddSchemeMethod.MakeGenericMethod(ArmTOptions, CliAuthHandler.MakeGenericType(ArmTOptions));
- var method = Expression.Call(builder, genMethod, scheme, action);
- var lambda = Expression.Lambda>(method, builder);
- return lambda.Compile();
- }
-
- internal static FunctionsAuthenticationBuilder AddScriptAuthLevel(FunctionsAuthenticationBuilder builder)
- {
- __authLDisabled(builder);
- return builder;
- }
-
- internal static FunctionsAuthenticationBuilder AddArmToken(FunctionsAuthenticationBuilder builder)
- {
- __armTokenDisabled(builder);
- return builder;
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/EnabledAuthHelper.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/EnabledAuthHelper.cs
deleted file mode 100644
index f71d399..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/EnabledAuthHelper.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using System.Collections.Generic;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Text;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal class EnabledAuthHelper : AuthHelper
- {
- private static Func __authNFunc = BuildAuthenticationFunc();
-
- private static Func BuildAuthenticationFunc()
- {
- var services = Expression.Parameter(typeof(IServiceCollection), "services");
- var method = Expression.Call(WebHostSvcCollectionExtType, "AddWebJobsScriptHostAuthentication", Type.EmptyTypes, services);
- var lambda = Expression.Lambda>(method, services);
-
- return lambda.Compile();
- }
-
- internal static void AddBuiltInFunctionsAuthentication(IServiceCollection services)
- {
- __authNFunc(services);
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionAuthorizationContext.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionAuthorizationContext.cs
deleted file mode 100644
index 80c8ea0..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionAuthorizationContext.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal class FunctionAuthorizationContext
- {
- public FunctionAuthorizationContext(HttpContext httpContext)
- {
- HttpContext = httpContext;
- }
-
- public HttpContext HttpContext { get; }
-
- public IActionResult? Result { get; internal set; }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthorizationOptions.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthorizationOptions.cs
deleted file mode 100644
index b0d5e10..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthorizationOptions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- ///
- /// Options to manage Authorization functionality for Azure Functions.
- ///
- public class FunctionsAuthorizationOptions
- {
- ///
- /// Gets or sets a value indicating whether authorization is disabled.
- ///
- public bool AuthorizationDisabled {get; set;}
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsHttpAuthorizationHandler.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsHttpAuthorizationHandler.cs
deleted file mode 100644
index e3265e7..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsHttpAuthorizationHandler.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Azure.WebJobs.Host;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal class FunctionsHttpAuthorizationHandler : IFunctionsHttpAuthorizationHandler
- {
- private readonly IFunctionsAuthorizationFilterIndex _filtersIndex;
-
- public FunctionsHttpAuthorizationHandler(IFunctionsAuthorizationFilterIndex filtersIndex)
- {
- _filtersIndex = filtersIndex ?? throw new ArgumentNullException(nameof(filtersIndex));
- }
-
- [Obsolete("This functionality is dependent on Azure Functions preview features.")]
- public async Task OnAuthorizingFunctionInstance(FunctionExecutingContext functionContext, HttpContext httpContext)
- {
- if (functionContext is null) throw new ArgumentNullException(nameof(functionContext));
- if (httpContext is null) throw new ArgumentNullException(nameof(httpContext));
-
- var filter = _filtersIndex.GetAuthorizationFilter(functionContext.FunctionName);
-
- if (filter is null) return;
-
- var context = new FunctionAuthorizationContext(httpContext);
-
- await filter.AuthorizeAsync(context);
-
- if (context.Result is ChallengeResult challenge)
- {
- if (!httpContext.Response.HasStarted)
- {
- if (challenge.AuthenticationSchemes != null && challenge.AuthenticationSchemes.Count > 0)
- {
- foreach (var scheme in challenge.AuthenticationSchemes)
- {
- await httpContext.ChallengeAsync(scheme);
- }
- }
- else
- {
- await httpContext.ChallengeAsync();
- }
-
- await SetResponseAsync("Unauthorized", httpContext.Response);
- }
-
- // need to make sure function stops executing. At this moment this is the only way.
- BombFunctionInstance(HttpStatusCode.Unauthorized);
- }
-
- if (context.Result is ForbidResult forbid)
- {
- if (!httpContext.Response.HasStarted)
- {
- if (forbid.AuthenticationSchemes != null && forbid.AuthenticationSchemes.Count > 0)
- {
- foreach (var scheme in forbid.AuthenticationSchemes)
- {
- await httpContext.ForbidAsync(scheme);
- }
- }
- else
- {
- await httpContext.ForbidAsync();
- }
-
- await SetResponseAsync("Forbidden", httpContext.Response);
- }
-
- // need to make sure function stops executing. At this moment this is the only way.
- BombFunctionInstance(HttpStatusCode.Forbidden);
- }
- }
-
- private async Task SetResponseAsync(string message, HttpResponse response)
- {
- if (response.HasStarted)
- return;
-
- response.ContentType = "text/plain";
- response.ContentLength = message.Length;
- await response.WriteAsync(message);
- await response.Body.FlushAsync();
- }
-
- private void BombFunctionInstance(HttpStatusCode status)
- {
- throw new FunctionAuthorizationException(status);
- }
- }
-}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/IFunctionsHttpAuthorizationHandler.cs b/src/DarkLoop.Azure.Functions.Authorize/Security/IFunctionsHttpAuthorizationHandler.cs
deleted file mode 100644
index 1437fe1..0000000
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/IFunctionsHttpAuthorizationHandler.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Azure.WebJobs.Host;
-
-namespace DarkLoop.Azure.Functions.Authorize.Security
-{
- internal interface IFunctionsHttpAuthorizationHandler
- {
- [Obsolete("This functionality is dependent on Azure Functions preview features.")]
- Task OnAuthorizingFunctionInstance(FunctionExecutingContext functionContext, HttpContext httpContext);
- }
-}
diff --git a/src/abstractions/Cache/FunctionsAuthorizationFilterCache.cs b/src/abstractions/Cache/FunctionsAuthorizationFilterCache.cs
new file mode 100644
index 0000000..ca7d7c3
--- /dev/null
+++ b/src/abstractions/Cache/FunctionsAuthorizationFilterCache.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Collections.Concurrent;
+
+namespace DarkLoop.Azure.Functions.Authorization.Cache
+{
+ ///
+ internal class FunctionsAuthorizationFilterCache : IFunctionsAuthorizationFilterCache
+ where TIdentifier : notnull
+ {
+ private readonly ConcurrentDictionary _filters = new();
+
+ ///
+ public bool TryGetFilter(TIdentifier functionIdentifier, out FunctionAuthorizationFilter? filter)
+ {
+ return _filters.TryGetValue(functionIdentifier, out filter);
+ }
+
+ ///
+ public bool SetFilter(TIdentifier functionIdentifier, FunctionAuthorizationFilter builder)
+ {
+ return _filters.TryAdd(functionIdentifier, builder);
+ }
+ }
+}
diff --git a/src/abstractions/Cache/IFunctionsAuthorizationFilterCache.cs b/src/abstractions/Cache/IFunctionsAuthorizationFilterCache.cs
new file mode 100644
index 0000000..693138a
--- /dev/null
+++ b/src/abstractions/Cache/IFunctionsAuthorizationFilterCache.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+namespace DarkLoop.Azure.Functions.Authorization.Cache
+{
+ ///
+ /// Provides a fully built authorization filter cache for functions.
+ ///
+ public interface IFunctionsAuthorizationFilterCache
+ {
+ ///
+ /// Gets the authorization filter for the specified function if exists.
+ ///
+ bool TryGetFilter(TIdentifier functionIdentifier, out FunctionAuthorizationFilter? filter);
+
+ ///
+ /// Sets the authorization filter for the specified function.
+ ///
+ /// The function unique identifier
+ ///
+ ///
+ bool SetFilter(TIdentifier functionIdentifier, FunctionAuthorizationFilter filter);
+ }
+}
\ No newline at end of file
diff --git a/src/abstractions/Constants.cs b/src/abstractions/Constants.cs
new file mode 100644
index 0000000..233e824
--- /dev/null
+++ b/src/abstractions/Constants.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ [ExcludeFromCodeCoverage]
+ internal class Constants
+ {
+ internal const string AuthInvokedKey = "__WebJobAuthInvoked";
+ internal const string WebJobsAuthScheme = "WebJobsAuthLevel";
+ internal const string ArmTokenAuthScheme = "ArmToken";
+ }
+}
diff --git a/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj b/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj
new file mode 100644
index 0000000..e4d1ca1
--- /dev/null
+++ b/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj
@@ -0,0 +1,42 @@
+
+
+
+ DarkLoop.Azure.Functions.Authorization.Abstractions
+ DarkLoop.Azure.Functions.Authorization
+ net6.0;net7.0
+ 0.0.1-preview
+ DarkLoop's Azure Functions authorization extension shared core functionality for InProc and Isolated modules.
+ enable
+
+
+
+ TRACE
+
+
+
+ DEBUG;TRACE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/abstractions/FunctionAuthorizationContext.cs b/src/abstractions/FunctionAuthorizationContext.cs
new file mode 100644
index 0000000..70b5e60
--- /dev/null
+++ b/src/abstractions/FunctionAuthorizationContext.cs
@@ -0,0 +1,61 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Diagnostics.CodeAnalysis;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Policy;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Represents the context associated with the current request authorization.
+ ///
+ ///
+ [ExcludeFromCodeCoverage]
+ public class FunctionAuthorizationContext
+ where TContext : class
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the function.
+ /// The platform dependent context associated with the current request.
+ /// The authorization policy associated with the current request.
+ /// The authorization result associated with the current request.
+ public FunctionAuthorizationContext(
+ string functionName, TContext httpContext, AuthorizationPolicy policy, PolicyAuthorizationResult result)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+ Check.NotNull(httpContext, nameof(httpContext));
+ Check.NotNull(policy, nameof(policy));
+ Check.NotNull(result, nameof(result));
+
+ FunctionName = functionName;
+ UnderlyingContext = httpContext;
+ Policy = policy;
+ Result = result;
+ }
+
+ ///
+ /// Gets the name of the function.
+ ///
+ public string FunctionName { get; }
+
+ ///
+ /// Gets the underlying context associated with the current request.
+ ///
+ public TContext UnderlyingContext { get; }
+
+ ///
+ /// Gets the authorization policy associated with the current request.
+ ///
+ public AuthorizationPolicy Policy { get; set; }
+
+ ///
+ /// Gets the authorization result associated with the current request.
+ ///
+ public PolicyAuthorizationResult Result { get; set; }
+ }
+}
diff --git a/src/abstractions/FunctionAuthorizationFilter.cs b/src/abstractions/FunctionAuthorizationFilter.cs
new file mode 100644
index 0000000..0e139b1
--- /dev/null
+++ b/src/abstractions/FunctionAuthorizationFilter.cs
@@ -0,0 +1,37 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Authorization;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Represents the authorization filter for a function.
+ ///
+ [ExcludeFromCodeCoverage]
+ public sealed class FunctionAuthorizationFilter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to be used for the function.
+ /// A value indicating whether the function allows anonymous access.
+ internal FunctionAuthorizationFilter(AuthorizationPolicy? authorizationPolicy, bool allowAnonymous = false)
+ {
+ Policy = authorizationPolicy;
+ AllowAnonymous = allowAnonymous;
+ }
+
+ ///
+ /// A value indicating whether the function allows anonymous access.
+ ///
+ public bool AllowAnonymous { get; }
+
+ ///
+ /// Gets or sets the to be used for the function.
+ ///
+ public AuthorizationPolicy? Policy { get; }
+ }
+}
diff --git a/src/abstractions/FunctionAuthorizationMetadata.cs b/src/abstractions/FunctionAuthorizationMetadata.cs
new file mode 100644
index 0000000..55e8c88
--- /dev/null
+++ b/src/abstractions/FunctionAuthorizationMetadata.cs
@@ -0,0 +1,130 @@
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ public sealed class FunctionAuthorizationMetadata
+ {
+ private readonly int _key;
+ private readonly Type? _functionType;
+ private readonly string? _functionName;
+ private readonly List _authData;
+
+ ///
+ /// Default authorization rule.
+ ///
+ internal readonly static FunctionAuthorizationMetadata Empty = new() { AllowsAnonymousAccess = true };
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private FunctionAuthorizationMetadata() => _authData = new List();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the function as specified in the [Function(Name)Attribute].
+ /// The type declaring the function method.
+ internal FunctionAuthorizationMetadata(string functionName, Type declaringType) : this()
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName), "The name of the function must be specified.");
+ Check.NotNull(declaringType, nameof(declaringType), "The declaring type of the function must be specified.");
+
+ _functionName = functionName;
+ _functionType = declaringType;
+ _key = GetId(functionName, declaringType);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type declaring the function method.
+ internal FunctionAuthorizationMetadata(Type declaringType) : this()
+ {
+ Check.NotNull(declaringType, nameof(declaringType), "The declaring type of the function must be specified.");
+
+ _functionType = declaringType;
+ _key = GetId(null, declaringType);
+ }
+
+ ///
+ /// Gets the authorization ID for the function or type.
+ ///
+ public int AuthorizationId => _key;
+
+ ///
+ /// Gets the name of the function as specified in the [Function(Name)Attribute].
+ ///
+ public string? FunctionName => _functionName;
+
+ ///
+ /// Gets the name of the type declaring the function method.
+ ///
+ public Type DeclaringType => _functionType!;
+
+ ///
+ /// Gets a value indicating whether the function allows anonymous access.
+ ///
+ public bool AllowsAnonymousAccess { get; internal set; }
+
+ ///
+ /// Gets the authorization data for the function or type.
+ ///
+ public IReadOnlyList AuthorizationData => _authData.ToImmutableArray();
+
+ ///
+ /// Adds authorization data to metadata.
+ ///
+ /// Authorize data.
+ public FunctionAuthorizationMetadata AddAuthorizeData(IAuthorizeData authorizeData)
+ {
+ Check.NotNull(authorizeData, nameof(authorizeData), "The authorization data must be specified.");
+
+ _authData.Add(authorizeData);
+
+ return this;
+ }
+
+ ///
+ /// Adds authorization data to metadata.
+ ///
+ /// Authorize data.
+ public FunctionAuthorizationMetadata AddAuthorizeData(IEnumerable authorizeData)
+ {
+ Check.NotNull(authorizeData, nameof(authorizeData), "The authorization data must be specified.");
+ Check.All(authorizeData, x => Check.NotNull(x, nameof(authorizeData), "All elements in authorization data must be non-null."));
+
+ _authData.AddRange(authorizeData);
+
+ return this;
+ }
+
+ ///
+ /// Allows anonymous access to the function.
+ /// Once the value ise set, it cannot be changed.
+ ///
+ public FunctionAuthorizationMetadata AllowAnonymousAccess()
+ {
+ AllowsAnonymousAccess = true;
+
+ return this;
+ }
+
+ ///
+ /// Gets the metadata ID for a function.
+ ///
+ /// The name of the function.
+ /// The type declaring the function.
+ internal static int GetId(string? functionName, Type? declaringType) =>
+ (declaringType?.GetHashCode() ?? 0) ^
+ (functionName?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0);
+ }
+}
diff --git a/src/abstractions/FunctionAuthorizationMetadataCollection.cs b/src/abstractions/FunctionAuthorizationMetadataCollection.cs
new file mode 100644
index 0000000..76d4dad
--- /dev/null
+++ b/src/abstractions/FunctionAuthorizationMetadataCollection.cs
@@ -0,0 +1,109 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Function authorization rule store to allow rules to be defined even outside of attributes
+ /// covering the Isolated hosting model.
+ ///
+ internal sealed class FunctionAuthorizationMetadataCollection
+ {
+ private readonly object _syncLock = new();
+ private readonly IDictionary _items =
+ new Dictionary();
+
+ ///
+ /// Adds a rule for all functions on the declaring type.
+ ///
+ /// The type containing the functions.
+ /// A value indicating whether the function declaring type is already registered.
+ /// An instance of .
+ internal FunctionAuthorizationMetadata Add(Type declaringType, out bool existing)
+ {
+ return this.GetOrAdd(null, declaringType, out existing);
+ }
+
+ ///
+ /// Adds a rule for a specific function on the declaring type.
+ ///
+ /// The name of the function.
+ /// The type declaring the function method.
+
+ /// An instance of
+ ///
+ /// When a rule already exists and defines a different value than the one specified in
+ ///
+ internal FunctionAuthorizationMetadata Add(string functionName, Type declaringType)
+ {
+ return this.GetOrAdd(functionName, declaringType, out _);
+ }
+
+ ///
+ /// Retrieves the function authorization metadata for a specific function on the declaring type.
+ ///
+ /// The name of the function.
+ /// The type declaring the function.
+ /// An instance of
+ internal FunctionAuthorizationMetadata GetMetadata(string functionName, Type declaringType)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+ Check.NotNull(declaringType, nameof(declaringType));
+
+ var typeId = FunctionAuthorizationMetadata.GetId(null, declaringType);
+ var functionId = FunctionAuthorizationMetadata.GetId(functionName, declaringType);
+
+ _items.TryGetValue(functionId, out var functionRule);
+ _items.TryGetValue(typeId, out var typeRule);
+
+ var typeAuthData = typeRule?.AuthorizationData ?? Enumerable.Empty();
+ var functionAuthData = functionRule?.AuthorizationData ?? Enumerable.Empty();
+
+ var merged = new FunctionAuthorizationMetadata(functionName, declaringType)
+ {
+ AllowsAnonymousAccess = functionRule?.AllowsAnonymousAccess ?? (typeRule?.AllowsAnonymousAccess ?? false)
+ };
+
+ return merged.AddAuthorizeData(typeAuthData.Concat(functionAuthData));
+ }
+
+ private FunctionAuthorizationMetadata GetOrAdd(string? functionName, Type declaringType, out bool existing)
+ {
+ var key = FunctionAuthorizationMetadata.GetId(functionName, declaringType);
+
+ existing = false;
+
+ if (!_items.TryGetValue(key, out var metadata))
+ {
+ lock (_syncLock)
+ {
+ if (!_items.TryGetValue(key, out metadata))
+ {
+ metadata = string.IsNullOrWhiteSpace(functionName)
+ ? new FunctionAuthorizationMetadata(declaringType)
+ : new FunctionAuthorizationMetadata(functionName, declaringType);
+
+ _items.Add(key, metadata);
+ }
+ else
+ {
+ existing = true;
+ }
+ }
+ }
+ else
+ {
+ existing = true;
+ }
+
+ return metadata;
+ }
+ }
+}
diff --git a/src/abstractions/FunctionAuthorizationTypeMap.cs b/src/abstractions/FunctionAuthorizationTypeMap.cs
new file mode 100644
index 0000000..5703427
--- /dev/null
+++ b/src/abstractions/FunctionAuthorizationTypeMap.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ internal sealed class FunctionAuthorizationTypeMap
+ {
+ private readonly ConcurrentDictionary _typeMap = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Registers the type declaring a function with the specified .
+ ///
+ /// The name of the function.
+ /// The type declaring the function.
+ /// if the registration was successful; otherwise .
+ internal bool AddFunctionType(string functionName, Type functionType)
+ {
+ return _typeMap.TryAdd(functionName, functionType);
+ }
+
+ ///
+ /// Gets the type declaring a function with the specified .
+ ///
+ /// The name of the function.
+ ///
+ internal Type? this[string functionName]
+ {
+ get => _typeMap.GetValueOrDefault(functionName);
+ }
+
+ ///
+ /// Returns a value indicating whether the function with the specified is registered.
+ ///
+ /// The name of the function.
+ /// if function is registered; otherwise
+ internal bool IsFunctionRegistered(string functionName)
+ {
+ return _typeMap.ContainsKey(functionName);
+ }
+ }
+}
diff --git a/src/abstractions/FunctionsAuthenticationBuilder.cs b/src/abstractions/FunctionsAuthenticationBuilder.cs
new file mode 100644
index 0000000..2f9531d
--- /dev/null
+++ b/src/abstractions/FunctionsAuthenticationBuilder.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// An that enhances the built-in authentication behavior for Azure Functions.
+ ///
+ public class FunctionsAuthenticationBuilder : AuthenticationBuilder
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The current service collection instance.
+ internal FunctionsAuthenticationBuilder(IServiceCollection services)
+ : base(services) { }
+ }
+}
diff --git a/src/abstractions/FunctionsAuthorizationBuilder.cs b/src/abstractions/FunctionsAuthorizationBuilder.cs
new file mode 100644
index 0000000..2719124
--- /dev/null
+++ b/src/abstractions/FunctionsAuthorizationBuilder.cs
@@ -0,0 +1,10 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ public class FunctionsAuthorizationBuilder
+ {
+ }
+}
diff --git a/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs b/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs
new file mode 100644
index 0000000..12b2ea8
--- /dev/null
+++ b/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization.Cache;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Extension methods for adding the Functions Authorization Core services to the DI container.
+ ///
+ internal static class FunctionsAuthorizationCoreServiceCollectionExtensions
+ {
+ public static IServiceCollection AddFunctionsAuthorizationCore(this IServiceCollection services)
+ {
+ services
+ .TryAddSingleton();
+
+ return services
+ .AddSingleton()
+ .AddSingleton(typeof(IFunctionsAuthorizationFilterCache<>), typeof(FunctionsAuthorizationFilterCache<>));
+ }
+ }
+}
diff --git a/src/abstractions/FunctionsAuthorizationOptions.cs b/src/abstractions/FunctionsAuthorizationOptions.cs
new file mode 100644
index 0000000..d948b0f
--- /dev/null
+++ b/src/abstractions/FunctionsAuthorizationOptions.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Options to manage Authorization functionality for Azure Functions.
+ ///
+ public class FunctionsAuthorizationOptions
+ {
+ internal readonly FunctionAuthorizationMetadataCollection AuthorizationMetadata = new();
+ internal readonly FunctionAuthorizationTypeMap TypeMap = new();
+
+ ///
+ /// Gets or sets a value indicating whether authorization is disabled.
+ ///
+ public bool AuthorizationDisabled {get; set;}
+
+ ///
+ /// Gets or sets a value indicating whether to write the HTTP status
+ /// to the response when authorization failure occurs.
+ ///
+ public bool WriteHttpStatusToResponse { get; set; }
+ }
+}
diff --git a/src/abstractions/FunctionsAuthorizationProvider.cs b/src/abstractions/FunctionsAuthorizationProvider.cs
new file mode 100644
index 0000000..17c2feb
--- /dev/null
+++ b/src/abstractions/FunctionsAuthorizationProvider.cs
@@ -0,0 +1,76 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorization.Cache;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ internal class FunctionsAuthorizationProvider : IFunctionsAuthorizationProvider
+ {
+ private static readonly IEnumerable __dismissedSchemes =
+ new[] { Constants.WebJobsAuthScheme, Constants.ArmTokenAuthScheme };
+
+
+ private readonly IFunctionsAuthorizationFilterCache _filterCache;
+ private readonly IAuthenticationSchemeProvider _schemesProvider;
+ private readonly IAuthorizationPolicyProvider _policyProvider;
+ private readonly FunctionsAuthorizationOptions _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ public FunctionsAuthorizationProvider(
+ IFunctionsAuthorizationFilterCache cache,
+ IAuthenticationSchemeProvider schemeProvider,
+ IAuthorizationPolicyProvider policyProvider,
+ IOptions options)
+ {
+ _filterCache = cache;
+ _schemesProvider = schemeProvider;
+ _policyProvider = policyProvider;
+ _options = options.Value;
+ }
+
+ ///
+ public async Task GetAuthorizationAsync(string functionName)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+
+ var declaringType = _options.GetFunctionDeclaringType(functionName);
+
+ var key = FunctionAuthorizationMetadata.GetId(functionName, declaringType);
+
+ if (_filterCache.TryGetFilter(key, out var filter))
+ {
+ return filter!;
+ }
+
+ var functionRule = _options.GetMetadata(functionName);
+
+ if (functionRule is null)
+ {
+ filter = new FunctionAuthorizationFilter(null, true);
+ }
+ else
+ {
+ var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, functionRule.AuthorizationData);
+
+ filter = new FunctionAuthorizationFilter(policy, functionRule.AllowsAnonymousAccess);
+ }
+
+ _filterCache.SetFilter(key, filter);
+
+ return filter;
+ }
+ }
+}
diff --git a/src/abstractions/FunctionsAuthorizationResultHandler.cs b/src/abstractions/FunctionsAuthorizationResultHandler.cs
new file mode 100644
index 0000000..1f0b035
--- /dev/null
+++ b/src/abstractions/FunctionsAuthorizationResultHandler.cs
@@ -0,0 +1,115 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization.Policy;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ internal class FunctionsAuthorizationResultHandler : IFunctionsAuthorizationResultHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The framework options.
+ public FunctionsAuthorizationResultHandler(
+ IOptionsMonitor monitoredOptions)
+ {
+ _options = monitoredOptions;
+ }
+
+ private FunctionsAuthorizationOptions Options => _options.CurrentValue;
+
+ ///
+ public async Task HandleResultAsync(
+ FunctionAuthorizationContext context,
+ HttpContext httpContext,
+ Func? onSuccess = null)
+ where TContext : class
+ {
+ Check.NotNull(context, nameof(context));
+ Check.NotNull(httpContext, nameof(httpContext));
+
+ if (context.Result.Succeeded)
+ {
+ if (onSuccess is not null)
+ {
+ await onSuccess(context.UnderlyingContext);
+ }
+
+ return;
+ }
+
+ if (context.Result.Challenged)
+ {
+ if (context.Policy.AuthenticationSchemes.Count > 0)
+ {
+ foreach (var scheme in context.Policy.AuthenticationSchemes)
+ {
+ await httpContext.ChallengeAsync(scheme);
+ }
+ }
+ else
+ {
+ await httpContext.ChallengeAsync();
+ }
+ }
+ else if (context.Result.Forbidden)
+ {
+ if (context.Policy.AuthenticationSchemes.Count > 0)
+ {
+ foreach (var scheme in context.Policy.AuthenticationSchemes)
+ {
+ await httpContext.ForbidAsync(scheme);
+ }
+ }
+ else
+ {
+ await httpContext.ForbidAsync();
+ }
+ }
+
+ await HandleFailureAsync(httpContext, context.Result);
+ }
+
+ // Writing default results for forbidden and challenged
+ private async Task HandleFailureAsync(HttpContext context, PolicyAuthorizationResult result)
+ {
+ if (Options.WriteHttpStatusToResponse && !context.Response.HasStarted)
+ {
+ var httpResult = default(IActionResult);
+
+ if (result.Forbidden)
+ {
+ httpResult = new AuthorizationFailureResult(HttpStatusCode.Forbidden);
+ }
+ else if (result.Challenged)
+ {
+ httpResult = new AuthorizationFailureResult(HttpStatusCode.Unauthorized);
+ }
+
+ await httpResult!.ExecuteResultAsync(new ActionContext(context, context.GetRouteData(), new ActionDescriptor()));
+ }
+ }
+
+ private class AuthorizationFailureResult : ObjectResult
+ {
+ public AuthorizationFailureResult(HttpStatusCode statusCode) : base(statusCode.ToString())
+ {
+ }
+ }
+ }
+}
diff --git a/src/abstractions/IFunctionsAuthorizationProvider.cs b/src/abstractions/IFunctionsAuthorizationProvider.cs
new file mode 100644
index 0000000..ddddff2
--- /dev/null
+++ b/src/abstractions/IFunctionsAuthorizationProvider.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Threading.Tasks;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Provides a bridge between Authorization rules and filter cache.
+ ///
+ internal interface IFunctionsAuthorizationProvider
+ {
+ ///
+ /// Returns the authorization filter for the given function.
+ ///
+ /// The name of the function.
+ ///
+ Task GetAuthorizationAsync(string functionName);
+ }
+}
diff --git a/src/abstractions/IFunctionsAuthorizationResultHandler.cs b/src/abstractions/IFunctionsAuthorizationResultHandler.cs
new file mode 100644
index 0000000..8505cda
--- /dev/null
+++ b/src/abstractions/IFunctionsAuthorizationResultHandler.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Handles the result of the authorization process.
+ ///
+ internal interface IFunctionsAuthorizationResultHandler
+ {
+ ///
+ /// Handles the result of the authorization process.
+ ///
+ /// The function authorization context.
+ /// The for the current request.
+ /// The action to execute if the authorization process succeeded.
+ ///
+ Task HandleResultAsync(
+ FunctionAuthorizationContext authorizationContext, HttpContext httpContext, Func? onSuccess = null)
+ where TContext : class;
+ }
+}
\ No newline at end of file
diff --git a/src/abstractions/Internal/Check.cs b/src/abstractions/Internal/Check.cs
new file mode 100644
index 0000000..5a9591b
--- /dev/null
+++ b/src/abstractions/Internal/Check.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace DarkLoop.Azure.Functions.Authorization.Internal
+{
+ [ExcludeFromCodeCoverage]
+ internal class Check
+ {
+ internal static void NotNull(object value, string name, string? message = null)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(name, message);
+ }
+ }
+
+ internal static void NotNullOrWhiteSpace(string value, string name, string? message = null)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException(message, name);
+ }
+ }
+
+ internal static void All(IEnumerable sequence, Action action)
+ {
+ foreach (var item in sequence)
+ {
+ action(item);
+ }
+ }
+ }
+}
diff --git a/src/abstractions/Internal/FunctionsAuthorizationOptionsExtensions.cs b/src/abstractions/Internal/FunctionsAuthorizationOptionsExtensions.cs
new file mode 100644
index 0000000..58b6c23
--- /dev/null
+++ b/src/abstractions/Internal/FunctionsAuthorizationOptionsExtensions.cs
@@ -0,0 +1,130 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ // This functionality can be exposed later through a builder to define the authorization rules for the functions
+ // without the need to use the attributes making it more performant and flexible.
+ internal static class FunctionsAuthorizationOptionsExtensions
+ {
+ ///
+ /// Registers all the functions in the specified .
+ ///
+ /// The type containing the functions.
+ /// A value indicating whether the function declaring type is already registered.
+ /// Return a to keep configuring.
+ internal static FunctionAuthorizationMetadata SetTypeAuthorizationInfo(
+ this FunctionsAuthorizationOptions options, Type declaringType, out bool existing)
+ {
+ Check.NotNull(declaringType, nameof(declaringType));
+
+ return options.AuthorizationMetadata.Add(declaringType, out existing);
+ }
+
+ ///
+ /// Registers the function with the specified name in
+ /// in the type specified in .
+ ///
+ /// The name of the function.
+ /// The type declaring the function.
+ /// Return a to keep configuring.
+ internal static FunctionAuthorizationMetadata SetFunctionAuthorizationInfo(
+ this FunctionsAuthorizationOptions options, string functionName, Type declaringType)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+ Check.NotNull(declaringType, nameof(declaringType));
+
+ options.RegisterFunctionDeclaringType(functionName, declaringType);
+
+ return options.AuthorizationMetadata.Add(functionName, declaringType);
+ }
+
+ ///
+ internal static bool IsFunctionRegistered(this FunctionsAuthorizationOptions options, string functionName)
+ {
+ return options.TypeMap.IsFunctionRegistered(functionName);
+ }
+
+ ///
+ /// Gets the authorization metadata for the specified function.
+ /// This method is intended to retrieve metadata at authorization time (not configuration).
+ ///
+ /// The name of the function
+ ///
+ internal static FunctionAuthorizationMetadata GetMetadata(
+ this FunctionsAuthorizationOptions options, string functionName)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+
+ var declaringType = options.GetFunctionDeclaringType(functionName);
+
+ if (declaringType is null)
+ {
+ return FunctionAuthorizationMetadata.Empty;
+ }
+
+ return options.AuthorizationMetadata.GetMetadata(functionName, declaringType);
+ }
+
+ internal static Type? GetFunctionDeclaringType(
+ this FunctionsAuthorizationOptions options, string functionName)
+ {
+ Check.NotNullOrWhiteSpace(functionName, nameof(functionName));
+
+ return options.TypeMap[functionName];
+ }
+
+ private static bool RegisterFunctionDeclaringType(
+ this FunctionsAuthorizationOptions options, string functionName, Type declaringType)
+ {
+ return options.TypeMap.AddFunctionType(functionName, declaringType);
+ }
+
+ internal static void RegisterFunctionAuthorizationMetadata(
+ this FunctionsAuthorizationOptions options, string functionName, Type declaringType, MethodInfo functionMethod)
+ where TAuthAttribute : Attribute, IAuthorizeData
+ {
+ var typeRule = options
+ .SetTypeAuthorizationInfo(declaringType, out var typeAlreadyRegistered);
+
+ if (!typeAlreadyRegistered)
+ {
+ var classAuthAttributes = declaringType.GetCustomAttributes().ToArray();
+ var classAllowAnonymous = declaringType.GetCustomAttribute();
+
+ if (classAuthAttributes.Length > 0)
+ {
+ typeRule.AddAuthorizeData(classAuthAttributes);
+ }
+
+ if (classAllowAnonymous is not null)
+ {
+ typeRule.AllowAnonymousAccess();
+ }
+ }
+
+ var methodAuthAttributes = functionMethod.GetCustomAttributes().ToArray();
+ var methodAllowAnonymous = functionMethod.GetCustomAttribute();
+ var methodRule = options
+ .SetFunctionAuthorizationInfo(functionName, declaringType);
+
+ if (methodAuthAttributes.Length > 0)
+ {
+ methodRule.AddAuthorizeData(methodAuthAttributes);
+ }
+
+ if (methodAllowAnonymous is not null)
+ {
+ methodRule.AllowAnonymousAccess();
+ }
+ }
+ }
+}
diff --git a/src/abstractions/README.md b/src/abstractions/README.md
new file mode 100644
index 0000000..24bea13
--- /dev/null
+++ b/src/abstractions/README.md
@@ -0,0 +1,7 @@
+# function-authorization-abstractions
+
+This module is a collection of abstractions to keep both authorization modules (Authorize and Authorization.Isolated) more in sync for future development.
+
+For more information on the dependent modules:
+- `DarkLoop.Azure.Functions.Authorize` read the following [README](https://github.com/dark-loop/functions-authorize/src/in-proc/README.md).
+- `DarkLoop.Azure.Functions.Authorization.Isolated` read the following [README](https://github.com/dark-loop/functions-authorize/src/isolated/README.md).
\ No newline at end of file
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationBuilderExtensions.cs b/src/abstractions/Security/AuthorizationBuilderExtensions.cs
similarity index 73%
rename from src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationBuilderExtensions.cs
rename to src/abstractions/Security/AuthorizationBuilderExtensions.cs
index 48ed5e6..463c706 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/AuthorizationBuilderExtensions.cs
+++ b/src/abstractions/Security/AuthorizationBuilderExtensions.cs
@@ -1,11 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using System.Text;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using DarkLoop.Azure.Functions.Authorization.Internal;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
-namespace DarkLoop.Azure.Functions.Authorize.Security
+namespace DarkLoop.Azure.Functions.Authorization
{
#if NET7_0_OR_GREATER
///
@@ -21,10 +23,7 @@ public static class AuthorizationBuilderExtensions
///
public static AuthorizationBuilder DisableAuthorization(this AuthorizationBuilder builder, bool disabled)
{
- if (builder is null)
- {
- throw new ArgumentNullException(nameof(builder));
- }
+ Check.NotNull(builder, nameof(builder));
builder.Services.Configure(options => options.AuthorizationDisabled = disabled);
return builder;
diff --git a/src/in-proc/.gitignore b/src/in-proc/.gitignore
new file mode 100644
index 0000000..ff5b00c
--- /dev/null
+++ b/src/in-proc/.gitignore
@@ -0,0 +1,264 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# Azure Functions localsettings file
+local.settings.json
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
\ No newline at end of file
diff --git a/src/in-proc/Bindings/FunctionsAuthorizeBindingProvider.cs b/src/in-proc/Bindings/FunctionsAuthorizeBindingProvider.cs
new file mode 100644
index 0000000..a9ddc59
--- /dev/null
+++ b/src/in-proc/Bindings/FunctionsAuthorizeBindingProvider.cs
@@ -0,0 +1,58 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Net.Http;
+using System.Reflection;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorize;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Host.Bindings;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization.Bindings
+{
+ internal class FunctionsAuthorizeBindingProvider : IBindingProvider
+ {
+ private readonly FunctionsAuthorizationOptions _options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ public FunctionsAuthorizeBindingProvider(
+ IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ ///
+ public Task TryCreateAsync(BindingProviderContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ var paramType = context.Parameter.ParameterType;
+ if (paramType == typeof(HttpRequest) || paramType == typeof(HttpRequestMessage))
+ {
+ this.ProcessAuthorization(context.Parameter);
+ }
+
+ return Task.FromResult(null);
+ }
+
+ private void ProcessAuthorization(ParameterInfo info)
+ {
+ var method = info.Member as MethodInfo ??
+ throw new InvalidOperationException($"Unable to bind authorization context for {info.Name}.");
+
+ var declaringType = method.DeclaringType!;
+ var nameAttr = method.GetCustomAttribute()!;
+
+ _options.RegisterFunctionAuthorizationMetadata(
+ nameAttr.Name, declaringType, method);
+ }
+ }
+}
diff --git a/src/in-proc/DarkLoop.Azure.Functions.Authorize.csproj b/src/in-proc/DarkLoop.Azure.Functions.Authorize.csproj
new file mode 100644
index 0000000..6eb4953
--- /dev/null
+++ b/src/in-proc/DarkLoop.Azure.Functions.Authorize.csproj
@@ -0,0 +1,73 @@
+
+
+
+ v3
+ DarkLoop.Azure.Functions.Authorization
+ DarkLoop.Azure.Functions.Authorize
+ net6.0;net7.0
+ 0.0.1-preview
+ 3472ff41-3859-4101-a2da-6c37d751fd31
+ Azure Functions V3 and V4 (InProc) extension to enable authentication and authorization on a per function basis based on ASPNET Core frameworks.
+ enable
+
+
+
+ TRACE
+
+
+
+ DEBUG;TRACE
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Strings.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Strings.Designer.cs
+
+
+
+
+
+ PreserveNewest
+ Never
+
+
+
diff --git a/src/in-proc/FunctionAuthorizationContextInternal.cs b/src/in-proc/FunctionAuthorizationContextInternal.cs
new file mode 100644
index 0000000..64399c0
--- /dev/null
+++ b/src/in-proc/FunctionAuthorizationContextInternal.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Policy;
+using Microsoft.AspNetCore.Http;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Internal implementation of the function authorization context.
+ ///
+ internal class FunctionAuthorizationContextInternal : FunctionAuthorizationContext
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal FunctionAuthorizationContextInternal(
+ string functionName, HttpContext httpContext, AuthorizationPolicy policy, PolicyAuthorizationResult result)
+ : base(functionName, httpContext, policy, result)
+ {
+ }
+ }
+}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizationException.cs b/src/in-proc/FunctionAuthorizationException.cs
similarity index 81%
rename from src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizationException.cs
rename to src/in-proc/FunctionAuthorizationException.cs
index 8703ab2..d3e77f0 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizationException.cs
+++ b/src/in-proc/FunctionAuthorizationException.cs
@@ -1,10 +1,12 @@
-using System;
-using System.Collections.Generic;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
using System.Net;
using System.Runtime.Serialization;
-using System.Text;
-namespace DarkLoop.Azure.Functions.Authorize
+namespace DarkLoop.Azure.Functions.Authorization
{
public sealed class FunctionAuthorizationException : Exception
{
diff --git a/src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizeAttribute.cs b/src/in-proc/FunctionAuthorizeAttribute.cs
similarity index 60%
rename from src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizeAttribute.cs
rename to src/in-proc/FunctionAuthorizeAttribute.cs
index 39663f6..5970ae0 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/FunctionAuthorizeAttribute.cs
+++ b/src/in-proc/FunctionAuthorizeAttribute.cs
@@ -1,13 +1,14 @@
-using System;
-using System.Linq;
-using System.Net.Http;
-using System.Runtime.ExceptionServices;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
using System.Threading;
using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using DarkLoop.Azure.Functions.Authorize.Security;
+using DarkLoop.Azure.Functions.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.DependencyInjection;
@@ -45,10 +46,13 @@ async Task IFunctionInvocationFilter.OnExecutingAsync(FunctionExecutingContext e
{
if (!this.IsProcessed(executingContext))
{
- var httpContext = this.GetHttpContext(executingContext);
- if (httpContext != null)
+ var httpContext = executingContext.GetHttpContext();
+ if (httpContext is not null)
{
- await AuthorizeRequestAsync(executingContext, httpContext);
+ var services = httpContext.RequestServices;
+ var authorizationExecutor = services.GetRequiredService();
+
+ await authorizationExecutor.ExecuteAuthorizationAsync(executingContext);
}
}
}
@@ -65,29 +69,5 @@ private bool IsProcessed(FunctionExecutingContext context)
return (bool)value;
}
-
- private HttpContext? GetHttpContext(FunctionExecutingContext context)
- {
- var requestOrMessage = context.Arguments.Values.FirstOrDefault(x => x is HttpRequest || x is HttpRequestMessage);
-
- if (requestOrMessage is HttpRequest request)
- {
- return request.HttpContext;
- }
- else if (requestOrMessage is HttpRequestMessage message)
- {
- return message.Properties[nameof(HttpContext)] as HttpContext;
- }
- else
- {
- return null;
- }
- }
-
- private async Task AuthorizeRequestAsync(FunctionExecutingContext functionContext, HttpContext httpContext)
- {
- var handler = httpContext.RequestServices.GetRequiredService();
- await handler.OnAuthorizingFunctionInstance(functionContext, httpContext);
- }
}
}
diff --git a/src/in-proc/FunctionExecutingContextExtensions.cs b/src/in-proc/FunctionExecutingContextExtensions.cs
new file mode 100644
index 0000000..1056709
--- /dev/null
+++ b/src/in-proc/FunctionExecutingContextExtensions.cs
@@ -0,0 +1,32 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Linq;
+using System.Net.Http;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.WebJobs.Host;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ internal static class FunctionExecutingContextExtensions
+ {
+ internal static HttpContext? GetHttpContext(this FunctionExecutingContext functionContext)
+ {
+ var requestOrMessage = functionContext.Arguments.Values.FirstOrDefault(x => x is HttpRequest || x is HttpRequestMessage);
+
+ if (requestOrMessage is HttpRequest request)
+ {
+ return request.HttpContext;
+ }
+ else if (requestOrMessage is HttpRequestMessage message)
+ {
+ return message.Properties[nameof(HttpContext)] as HttpContext;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/in-proc/FunctionsAuthExtension.cs b/src/in-proc/FunctionsAuthExtension.cs
new file mode 100644
index 0000000..952d526
--- /dev/null
+++ b/src/in-proc/FunctionsAuthExtension.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using Microsoft.Azure.WebJobs.Description;
+using Microsoft.Azure.WebJobs.Host.Config;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ [Extension("FunctionsAuthorize")]
+ class FunctionsAuthExtension : IExtensionConfigProvider
+ {
+ public void Initialize(ExtensionConfigContext context)
+ {
+ }
+ }
+}
diff --git a/src/in-proc/FunctionsAuthorizationExecutor.cs b/src/in-proc/FunctionsAuthorizationExecutor.cs
new file mode 100644
index 0000000..0648690
--- /dev/null
+++ b/src/in-proc/FunctionsAuthorizationExecutor.cs
@@ -0,0 +1,115 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Net;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization.Policy;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Azure.WebJobs.Host;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ internal sealed class FunctionsAuthorizationExecutor : IFunctionsAuthorizationExecutor
+ {
+ private readonly IFunctionsAuthorizationProvider _authorizationProvider;
+ private readonly IFunctionsAuthorizationResultHandler _authorizationHandler;
+ private readonly IPolicyEvaluator _policyEvaluator;
+ private readonly IOptionsMonitor _configOptions;
+ private readonly FunctionsAuthorizationOptions _options;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Functions authorization filters provider
+ /// An authorization handler
+ /// The policy evaluator
+ public FunctionsAuthorizationExecutor(
+ IFunctionsAuthorizationProvider authorizationProvider,
+ IFunctionsAuthorizationResultHandler authorizationHandler,
+ IPolicyEvaluator policyEvaluator,
+ IOptionsMonitor configOptions,
+ IOptions options,
+ ILogger logger)
+ {
+ Check.NotNull(authorizationProvider, nameof(authorizationProvider));
+ Check.NotNull(authorizationHandler, nameof(authorizationHandler));
+ Check.NotNull(policyEvaluator, nameof(policyEvaluator));
+ Check.NotNull(configOptions, nameof(configOptions));
+ Check.NotNull(logger, nameof(logger));
+
+ _authorizationProvider = authorizationProvider;
+ _authorizationHandler = authorizationHandler;
+ _policyEvaluator = policyEvaluator;
+ _configOptions = configOptions;
+ _options = options.Value;
+ _logger = logger;
+ }
+
+ ///
+ public async Task ExecuteAuthorizationAsync(FunctionExecutingContext context)
+ {
+ Check.NotNull(context, nameof(context));
+
+ var httpContext = context.GetHttpContext()!;
+
+ if (this._configOptions.CurrentValue.AuthorizationDisabled)
+ {
+ _logger.LogWarning(
+ $"Authorization through FunctionAuthorizeAttribute is disabled at the application level. Skipping authorization for {httpContext.Request.GetDisplayUrl()}.");
+
+ return;
+ }
+
+ var filter = await _authorizationProvider.GetAuthorizationAsync(context.FunctionName);
+
+ if (httpContext.Items.ContainsKey(Constants.AuthInvokedKey) ||
+ filter?.Policy is null)
+ {
+ return;
+ }
+
+ var authenticateResult = await _policyEvaluator.AuthenticateAsync(filter.Policy, httpContext);
+
+ // still authenticating in case token is sent to set context user but skipping authorization
+ if (filter.AllowAnonymous)
+ {
+ return;
+ }
+
+ var authorizeResult = await _policyEvaluator.AuthorizeAsync(filter.Policy, authenticateResult, httpContext, httpContext);
+ var authContext = new FunctionAuthorizationContextInternal(
+ context.FunctionName, httpContext, filter.Policy, authorizeResult);
+
+ await _authorizationHandler.HandleResultAsync(authContext, httpContext);
+
+ if (!authorizeResult.Succeeded)
+ {
+ if (authorizeResult.Challenged)
+ {
+ BombFunctionInstanceAsync(HttpStatusCode.Unauthorized);
+ }
+ else if (authorizeResult.Forbidden)
+ {
+ BombFunctionInstanceAsync(HttpStatusCode.Forbidden);
+ }
+ }
+ }
+
+ ///
+ /// Writes a failure message to response if nothing was written by framework handling functionality.
+ ///
+ /// The status to report back to caller.
+ ///
+ ///
+ private static void BombFunctionInstanceAsync(HttpStatusCode status)
+ {
+ throw new FunctionAuthorizationException(status);
+ }
+ }
+}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/FunctionsHosBuilderExtensions.cs b/src/in-proc/FunctionsAuthorizationHostBuilderExtensions.cs
similarity index 64%
rename from src/DarkLoop.Azure.Functions.Authorize/FunctionsHosBuilderExtensions.cs
rename to src/in-proc/FunctionsAuthorizationHostBuilderExtensions.cs
index d3f212b..6fc5933 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/FunctionsHosBuilderExtensions.cs
+++ b/src/in-proc/FunctionsAuthorizationHostBuilderExtensions.cs
@@ -1,15 +1,11 @@
-using DarkLoop.Azure.Functions.Authorize.Security;
-using Microsoft.Azure.Functions.Extensions.DependencyInjection;
-using System;
-using System.Collections.Generic;
-using System.Text;
+using DarkLoop.Azure.Functions.Authorization.Utils;
namespace Microsoft.Azure.Functions.Extensions.DependencyInjection
{
///
/// Extension methods for .
///
- public static class FunctionsHostBuilderExtensions
+ public static class FunctionsAuthorizationHostBuilderExtensions
{
///
/// Returns a value indicating whether the current environment is local development.
@@ -18,7 +14,7 @@ public static class FunctionsHostBuilderExtensions
///
public static bool IsLocalAuthorizationContext(this IFunctionsHostBuilder builder)
{
- return AuthHelper.IsLocalDevelopment;
+ return HostUtils.IsLocalDevelopment;
}
}
}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthorizeStartup.cs b/src/in-proc/FunctionsAuthorizeStartup.cs
similarity index 53%
rename from src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthorizeStartup.cs
rename to src/in-proc/FunctionsAuthorizeStartup.cs
index a619968..6c0d9ad 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/FunctionsAuthorizeStartup.cs
+++ b/src/in-proc/FunctionsAuthorizeStartup.cs
@@ -1,7 +1,9 @@
-using DarkLoop.Azure.Functions.Authorize;
-using DarkLoop.Azure.Functions.Authorize.Bindings;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using DarkLoop.Azure.Functions.Authorize.Security;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Bindings;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host.Bindings;
@@ -9,15 +11,17 @@
using Microsoft.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(FunctionsAuthorizeStartup))]
-namespace DarkLoop.Azure.Functions.Authorize
+
+namespace DarkLoop.Azure.Functions.Authorization
{
class FunctionsAuthorizeStartup : FunctionsStartup, IWebJobsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services
+ .AddFunctionsAuthorizationCore()
+ .AddSingleton()
+ .AddSingleton();
}
void IWebJobsStartup.Configure(IWebJobsBuilder builder)
diff --git a/src/in-proc/IFunctionsAuthorizationExecutor.cs b/src/in-proc/IFunctionsAuthorizationExecutor.cs
new file mode 100644
index 0000000..785a998
--- /dev/null
+++ b/src/in-proc/IFunctionsAuthorizationExecutor.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization.Policy;
+using Microsoft.Azure.WebJobs.Host;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ ///
+ /// Executes the authorization for a given function.
+ ///
+ internal interface IFunctionsAuthorizationExecutor
+ {
+ ///
+ /// Executes the authorization for a given function.
+ ///
+ /// The function authorization context.
+ Task ExecuteAuthorizationAsync(FunctionExecutingContext context);
+ }
+}
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Properties/Settings.cs b/src/in-proc/Properties/Settings.cs
similarity index 100%
rename from src/DarkLoop.Azure.Functions.Authorize/Properties/Settings.cs
rename to src/in-proc/Properties/Settings.cs
diff --git a/src/in-proc/Properties/Strings.Designer.cs b/src/in-proc/Properties/Strings.Designer.cs
new file mode 100644
index 0000000..f3e021c
--- /dev/null
+++ b/src/in-proc/Properties/Strings.Designer.cs
@@ -0,0 +1,144 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace DarkLoop.Azure.Functions.Authorization.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DarkLoop.Azure.Functions.Authorization.Properties.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Azure.Functions.Cli.Actions.HostActions.WebHost.Security.CliAuthenticationHandler`1.
+ ///
+ internal static string Func_ClieAuthHandler {
+ get {
+ return ResourceManager.GetString("Func_ClieAuthHandler", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AddWebJobsScriptHostAuthentication.
+ ///
+ internal static string WJH_AddAuthentication {
+ get {
+ return ResourceManager.GetString("WJH_AddAuthentication", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AddWebJobsScriptHostAuthorization.
+ ///
+ internal static string WJH_AddAuthorization {
+ get {
+ return ResourceManager.GetString("WJH_AddAuthorization", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.ArmAuthenticationOptions.
+ ///
+ internal static string WJH_ArmAuthOptions {
+ get {
+ return ResourceManager.GetString("WJH_ArmAuthOptions", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Azure.WebJobs.Script.WebHost.
+ ///
+ internal static string WJH_Assembly {
+ get {
+ return ResourceManager.GetString("WJH_Assembly", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Azure.WebJobs.Script.WebHost.Authentication.AuthenticationLevelOptions.
+ ///
+ internal static string WJH_AuthLevelOptions {
+ get {
+ return ResourceManager.GetString("WJH_AuthLevelOptions", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Azure.WebJobs.Script.WebHost.Features.IFunctionExecutionFeature.
+ ///
+ internal static string WJH_FuncExecFeature {
+ get {
+ return ResourceManager.GetString("WJH_FuncExecFeature", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Extensions.DependencyInjection.ScriptJwtBearerExtensions.
+ ///
+ internal static string WJH_JWTExtensions {
+ get {
+ return ResourceManager.GetString("WJH_JWTExtensions", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Microsoft.Azure.WebJobs.Script.WebHost.WebHostServiceCollectionExtensions.
+ ///
+ internal static string WJH_WebJovsSvcsExtensions {
+ get {
+ return ResourceManager.GetString("WJH_WebJovsSvcsExtensions", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/in-proc/Properties/Strings.resx b/src/in-proc/Properties/Strings.resx
new file mode 100644
index 0000000..82bb297
--- /dev/null
+++ b/src/in-proc/Properties/Strings.resx
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Azure.Functions.Cli.Actions.HostActions.WebHost.Security.CliAuthenticationHandler`1
+
+
+ AddWebJobsScriptHostAuthentication
+
+
+ AddWebJobsScriptHostAuthorization
+
+
+ Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication.ArmAuthenticationOptions
+
+
+ Microsoft.Azure.WebJobs.Script.WebHost
+
+
+ Microsoft.Azure.WebJobs.Script.WebHost.Authentication.AuthenticationLevelOptions
+
+
+ Microsoft.Azure.WebJobs.Script.WebHost.Features.IFunctionExecutionFeature
+
+
+ Microsoft.Extensions.DependencyInjection.ScriptJwtBearerExtensions
+
+
+ Microsoft.Azure.WebJobs.Script.WebHost.WebHostServiceCollectionExtensions
+
+
\ No newline at end of file
diff --git a/src/in-proc/Properties/serviceDependencies.json b/src/in-proc/Properties/serviceDependencies.json
new file mode 100644
index 0000000..df4dcc9
--- /dev/null
+++ b/src/in-proc/Properties/serviceDependencies.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights"
+ },
+ "storage1": {
+ "type": "storage",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/in-proc/Properties/serviceDependencies.local.json b/src/in-proc/Properties/serviceDependencies.local.json
new file mode 100644
index 0000000..b804a28
--- /dev/null
+++ b/src/in-proc/Properties/serviceDependencies.local.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights.sdk"
+ },
+ "storage1": {
+ "type": "storage.emulator",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/in-proc/README.md b/src/in-proc/README.md
new file mode 100644
index 0000000..1470f89
--- /dev/null
+++ b/src/in-proc/README.md
@@ -0,0 +1,81 @@
+# functions-authorize
+Bringing AuthorizeAttribute Behavior to Azure Functions v3 and v4 (In-Process)
+
+It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does.
+
+## Using the package
+### Installing the package
+`dotnet add package DarkLoop.Azure.Functions.Authorize`
+
+### Setting up authentication
+The goal is to utilize the same authentication framework provided for ASP.NET Core
+```csharp
+using Microsoft.Azure.Functions.Extensions.DependencyInjection;
+using MyFunctionAppNamespace;
+
+[assembly: FunctionsStartup(typeof(Startup))]
+namespace MyFunctionAppNamespace
+{
+ class Startup : FunctionsStartup
+ {
+ public void Configure(IFunctionsHostBuilder builder)
+ {
+ builder
+ .AddAuthentication(options =>
+ {
+ options.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddOpenIdConnect(options =>
+ {
+ options.ClientId = "";
+ // ... more options here
+ })
+ .AddJwtBearer(options =>
+ {
+ options.Audience = "";
+ // ... more options here
+ });
+
+ builder
+ .AddAuthorization(options =>
+ {
+ options.AddPolicy("OnlyAdmins", policyBuilder =>
+ {
+ // configure my policy requirements
+ });
+ });
+ }
+ }
+}
+```
+
+No need to register the middleware the way we do for ASP.NET Core applications.
+
+### Using the attribute
+And now lets use `FunctionAuthorizeAttribute` the same way we use `AuthorizeAttribute` in our ASP.NET Core applications.
+```csharp
+public class Functions
+{
+ [FunctionAuthorize]
+ [FunctionName("get-record")]
+ public async Task GetRecord(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
+ ILogger log)
+ {
+ var user = req.HttpContext.User;
+ var record = GetUserData(user.Identity.Name);
+ return new OkObjectResult(record);
+ }
+
+ [FunctionAuthorize(Policy = "OnlyAdmins")]
+ [FunctionName("get-all-records")]
+ public async Task GetAllRecords(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
+ ILogger log)
+ {
+ var records = GetAllData();
+ return new OkObjectResult(records);
+ }
+}
+```
\ No newline at end of file
diff --git a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthenticationBuilder.cs b/src/in-proc/Security/FunctionsAuthenticationBuilderExtensions.cs
similarity index 61%
rename from src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthenticationBuilder.cs
rename to src/in-proc/Security/FunctionsAuthenticationBuilderExtensions.cs
index 89855a7..6e0a212 100644
--- a/src/DarkLoop.Azure.Functions.Authorize/Security/FunctionsAuthenticationBuilder.cs
+++ b/src/in-proc/Security/FunctionsAuthenticationBuilderExtensions.cs
@@ -1,21 +1,17 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.Azure.Functions.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
using System;
using System.Linq;
+using DarkLoop.Azure.Functions.Authorization;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Options;
-namespace DarkLoop.Azure.Functions.Authorize.Security
+namespace Microsoft.Extensions.DependencyInjection
{
- ///
- /// An that enhances the built-in authentication behavior for Azure Functions.
- ///
- public class FunctionsAuthenticationBuilder : AuthenticationBuilder
+ public static class FunctionsAuthenticationBuilderExtensions
{
- internal FunctionsAuthenticationBuilder(IServiceCollection services)
- : base(services) { }
-
///
/// Adds the JWT Bearer scheme to the authentication configuration. JWT is added by default to Azure Functions
/// and all HTTP functions are applied the Admin level after a token is validated.
@@ -24,9 +20,10 @@ internal FunctionsAuthenticationBuilder(IServiceCollection services)
/// Bearer scheme is still in place, but Admin level is not set for incoming requests.
/// When setting this value to true (default) all existing configuration will be removed.
/// A instance of the
- public FunctionsAuthenticationBuilder AddJwtBearer(bool removeBuiltInConfig = true)
+ public static FunctionsAuthenticationBuilder AddJwtBearer(
+ this FunctionsAuthenticationBuilder builder, bool removeBuiltInConfig = true)
{
- return this.AddJwtBearer(delegate { }, removeBuiltInConfig);
+ return builder.AddJwtBearer(delegate { }, removeBuiltInConfig);
}
///
@@ -34,16 +31,17 @@ public FunctionsAuthenticationBuilder AddJwtBearer(bool removeBuiltInConfig = tr
/// and all HTTP functions are applied the Admin level after a token is validated.
///
/// An action configuring the JWT options for authentication.
- /// When is set to false, it enhances the built-in configuration for the scheme
+ /// When is set to , it enhances the built-in configuration for the scheme
/// A value indicating whether remove the built-in configuration for JWT.
/// Bearer scheme is still in place, but Admin level is not set incoming requests.
- /// When setting this value to true (default) all existing configuration will be removed.
+ /// When setting this value to (default) all existing configuration will be removed.
/// A instance of the
- public FunctionsAuthenticationBuilder AddJwtBearer(Action configureOptions, bool removeBuiltInConfig = true)
+ public static FunctionsAuthenticationBuilder AddJwtBearer(
+ this FunctionsAuthenticationBuilder builder, Action configureOptions, bool removeBuiltInConfig = true)
{
- if(removeBuiltInConfig)
+ if (removeBuiltInConfig)
{
- var descriptors = Services
+ var descriptors = builder.Services
.Where(s => s.ServiceType == typeof(IConfigureOptions))
.ToList();
@@ -53,16 +51,16 @@ public FunctionsAuthenticationBuilder AddJwtBearer(Action conf
if (instance?.Name == "Bearer")
{
- Services.Remove(descriptor);
+ builder.Services.Remove(descriptor!);
}
}
}
- this.Services
+ builder.Services
.AddOptions(JwtBearerDefaults.AuthenticationScheme)
.Configure(configureOptions);
- return this;
+ return builder;
}
}
}
diff --git a/src/in-proc/Security/FunctionsAuthenticationServiceCollectionExtensions.cs b/src/in-proc/Security/FunctionsAuthenticationServiceCollectionExtensions.cs
new file mode 100644
index 0000000..059f1fe
--- /dev/null
+++ b/src/in-proc/Security/FunctionsAuthenticationServiceCollectionExtensions.cs
@@ -0,0 +1,76 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using DarkLoop.Azure.Functions.Authorization.Utils;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class FunctionsAuthenticationServiceCollectionExtensions
+ {
+ ///
+ /// Adds Functions built-in authentication.
+ ///
+ public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, string? defaultScheme = null)
+ {
+ Check.NotNull(services, nameof(services));
+
+ return services.AddFunctionsAuthentication(defaultScheme, null);
+ }
+
+ ///
+ /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
+ ///
+ /// The configuration logic.
+ public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, Action? configure)
+ {
+ Check.NotNull(services, nameof(services));
+
+ return services.AddFunctionsAuthentication(null, configure);
+ }
+
+ private static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, string? defaultScheme, Action? configure)
+ {
+ var authBuilder = new FunctionsAuthenticationBuilder(services);
+
+ if (HostUtils.IsLocalDevelopment)
+ {
+ if (!string.IsNullOrWhiteSpace(defaultScheme))
+ {
+ services.AddAuthentication(defaultScheme!);
+ }
+ else
+ {
+ services.AddAuthentication();
+ }
+
+ LocalHostUtils.AddScriptJwtBearer(authBuilder);
+ LocalHostUtils.AddScriptAuthLevel(authBuilder);
+ LocalHostUtils.AddArmToken(authBuilder);
+ }
+ else
+ {
+ HostUtils.AddFunctionsBuiltInAuthentication(services);
+ }
+
+ if (string.IsNullOrWhiteSpace(defaultScheme) && configure is not null)
+ {
+ services.AddSingleton>(provider =>
+ new ConfigureOptions(options =>
+ {
+ configure(options);
+ }));
+ }
+
+ return authBuilder;
+ }
+ }
+}
diff --git a/src/in-proc/Security/FunctionsAuthorizationServiceCollectionExtensions.cs b/src/in-proc/Security/FunctionsAuthorizationServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3d88faa
--- /dev/null
+++ b/src/in-proc/Security/FunctionsAuthorizationServiceCollectionExtensions.cs
@@ -0,0 +1,41 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using DarkLoop.Azure.Functions.Authorization.Utils;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class FunctionsAuthorizationServiceCollectionExtensions
+ {
+ ///
+ /// Adds Functions built-in authorization.
+ ///
+ /// The service collection to configure.
+ public static IServiceCollection AddFunctionsAuthorization(this IServiceCollection services)
+ {
+ if (services is null) throw new ArgumentNullException(nameof(services));
+
+ return services.AddFunctionsAuthorization(delegate { });
+ }
+
+ ///
+ /// Adds Functions built-in authorization handlers and allows for further configuration.
+ ///
+ /// The service collection to configure.
+ /// The method to configure the authorization options.
+ public static IServiceCollection AddFunctionsAuthorization(
+ this IServiceCollection services, Action configure)
+ {
+ if (services is null) throw new ArgumentNullException(nameof(services));
+ if (configure is null) throw new ArgumentNullException(nameof(configure));
+
+ HostUtils.AddFunctionsBuiltInAuthorization(services);
+ services.Configure(configure);
+
+ return services;
+ }
+ }
+}
diff --git a/src/in-proc/Utils/HostUtils.cs b/src/in-proc/Utils/HostUtils.cs
new file mode 100644
index 0000000..bf084f5
--- /dev/null
+++ b/src/in-proc/Utils/HostUtils.cs
@@ -0,0 +1,86 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
+using DarkLoop.Azure.Functions.Authorization.Properties;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace DarkLoop.Azure.Functions.Authorization.Utils
+{
+ internal class HostUtils
+ {
+ protected static readonly Assembly WebJobsHostAssembly;
+
+ // These are a series of publicly available types that are used to interact with the Azure Functions runtime.
+ // We use reflection to access these types to not create a hard dependency on the Azure Functions WebHost.
+ private static readonly Type? __webHostSvcCollectionExtType;
+ internal static readonly Type? FunctionExecutionFeatureType;
+
+ private static readonly Func? __authorizationFunc;
+ private static readonly Func? __authenticationFunc;
+
+ static HostUtils()
+ {
+ WebJobsHostAssembly = Assembly.Load(Strings.WJH_Assembly);
+
+ if (WebJobsHostAssembly is null)
+ {
+ throw new InvalidOperationException($"{Assembly.GetExecutingAssembly()} cannot be used outside of an Azure Functions environment.");
+ }
+
+ __webHostSvcCollectionExtType = WebJobsHostAssembly.GetType(Strings.WJH_WebJovsSvcsExtensions);
+ FunctionExecutionFeatureType = WebJobsHostAssembly.GetType(Strings.WJH_FuncExecFeature);
+
+ var entryAssembly = Assembly.GetEntryAssembly();
+ var entryFullName = entryAssembly!.FullName;
+ var entryName = entryFullName!.Substring(0, entryFullName.IndexOf(','));
+ IsLocalDevelopment = !entryName.Equals(Strings.WJH_Assembly, StringComparison.OrdinalIgnoreCase);
+
+ __authenticationFunc = BuildBuiltInAuthenticationFunc();
+ __authorizationFunc = BuildBuiltInAuthorizationFunc();
+ }
+
+ internal static bool IsLocalDevelopment { get; }
+
+ internal static IServiceCollection AddFunctionsBuiltInAuthentication(IServiceCollection services)
+ {
+ return __authenticationFunc?.Invoke(services) ?? services;
+ }
+
+ internal static IServiceCollection AddFunctionsBuiltInAuthorization(IServiceCollection services)
+ {
+ return __authorizationFunc?.Invoke(services) ?? services;
+ }
+
+ private static Func BuildBuiltInAuthenticationFunc()
+ {
+ if (__webHostSvcCollectionExtType is not null)
+ {
+ var services = Expression.Parameter(typeof(IServiceCollection), "services");
+ var method = Expression.Call(__webHostSvcCollectionExtType, Strings.WJH_AddAuthentication, Type.EmptyTypes, services);
+ var lambda = Expression.Lambda>(method, services);
+
+ return lambda.Compile();
+ }
+
+ return builder => builder;
+ }
+
+ private static Func BuildBuiltInAuthorizationFunc()
+ {
+ if (__webHostSvcCollectionExtType is not null)
+ {
+ var services = Expression.Parameter(typeof(IServiceCollection), "services");
+ var method = Expression.Call(__webHostSvcCollectionExtType, Strings.WJH_AddAuthorization, Type.EmptyTypes, services);
+ var lambda = Expression.Lambda>(method, services);
+
+ return lambda.Compile();
+ }
+
+ return services => services;
+ }
+ }
+}
diff --git a/src/in-proc/Utils/LocalHostUtils.cs b/src/in-proc/Utils/LocalHostUtils.cs
new file mode 100644
index 0000000..cf2fa2e
--- /dev/null
+++ b/src/in-proc/Utils/LocalHostUtils.cs
@@ -0,0 +1,118 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
+using DarkLoop.Azure.Functions.Authorization.Properties;
+using Microsoft.AspNetCore.Authentication;
+
+namespace DarkLoop.Azure.Functions.Authorization.Utils
+{
+ internal class LocalHostUtils : HostUtils
+ {
+ private static readonly Assembly __funcAssembly;
+ private static readonly MethodInfo? __addSchemeMethod;
+
+ // The following types are used to interact with the Azure Functions runtime.
+ // We use reflection to access these types to not create a hard dependency on the Azure Functions Hosting.
+ private static readonly Type? __jwtSecurityExtensionsType;
+ private static readonly Type? __authLevelOptionsType;
+ private static readonly Type? __armTokenOptionsType;
+ private static readonly Type? __cliAuthHandlerType;
+ private static readonly Func? __addBuiltInJwt;
+ private static readonly Func? __addAuthLevel;
+ private static readonly Func? __addArmToken;
+
+ static LocalHostUtils()
+ {
+ __funcAssembly = Assembly.Load("func");
+
+ if (IsLocalDevelopment)
+ {
+ Expression addSchemeExpression = (AuthenticationBuilder b) =>
+ b.AddScheme>("scheme", null);
+
+ __addSchemeMethod = (((LambdaExpression)addSchemeExpression).Body as MethodCallExpression)!.Method.GetGenericMethodDefinition();
+ __jwtSecurityExtensionsType = WebJobsHostAssembly.GetType(Strings.WJH_JWTExtensions);
+ __authLevelOptionsType = WebJobsHostAssembly.GetType(Strings.WJH_AuthLevelOptions);
+ __armTokenOptionsType = WebJobsHostAssembly.GetType(Strings.WJH_ArmAuthOptions);
+ __cliAuthHandlerType = __funcAssembly.GetType(Strings.Func_ClieAuthHandler);
+
+ __addBuiltInJwt = BuildAddBuiltInJwtFunc();
+ __addAuthLevel = BuildAuthLevelFunc();
+ __addArmToken = BuildArmTokenFunc();
+ }
+ }
+
+ internal static FunctionsAuthenticationBuilder AddScriptJwtBearer(FunctionsAuthenticationBuilder builder)
+ {
+ __addBuiltInJwt?.Invoke(builder);
+
+ return builder;
+ }
+
+ internal static FunctionsAuthenticationBuilder AddScriptAuthLevel(FunctionsAuthenticationBuilder builder)
+ {
+ __addAuthLevel?.Invoke(builder);
+
+ return builder;
+ }
+
+ internal static FunctionsAuthenticationBuilder AddArmToken(FunctionsAuthenticationBuilder builder)
+ {
+ __addArmToken?.Invoke(builder);
+
+ return builder;
+ }
+
+ private static Func BuildAddBuiltInJwtFunc()
+ {
+ if (__jwtSecurityExtensionsType is not null)
+ {
+ var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
+ var method = Expression.Call(__jwtSecurityExtensionsType, "AddScriptJwtBearer", Type.EmptyTypes, builder);
+ var lambda = Expression.Lambda>(method, builder);
+
+ return lambda.Compile();
+ }
+
+ return builder => builder;
+ }
+
+ private static Func BuildAuthLevelFunc()
+ {
+ if (IsLocalDevelopment && __authLevelOptionsType is not null && __cliAuthHandlerType is not null)
+ {
+ var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
+ var scheme = Expression.Constant(Constants.WebJobsAuthScheme);
+ var action = Expression.Lambda(Expression.Empty(), Expression.Parameter(__authLevelOptionsType, "options"));
+ var genMethod = __addSchemeMethod!.MakeGenericMethod(__authLevelOptionsType, __cliAuthHandlerType.MakeGenericType(__authLevelOptionsType));
+ var method = Expression.Call(builder, genMethod, scheme, action);
+ var lambda = Expression.Lambda>(method, builder);
+
+ return lambda.Compile();
+ }
+
+ return builder => builder;
+ }
+
+ private static Func BuildArmTokenFunc()
+ {
+ if (IsLocalDevelopment && __armTokenOptionsType is not null && __cliAuthHandlerType is not null)
+ {
+ var builder = Expression.Parameter(typeof(AuthenticationBuilder), "builder");
+ var scheme = Expression.Constant(Constants.ArmTokenAuthScheme);
+ var action = Expression.Lambda(Expression.Empty(), Expression.Parameter(__armTokenOptionsType, "options"));
+ var genMethod = __addSchemeMethod!.MakeGenericMethod(__armTokenOptionsType, __cliAuthHandlerType.MakeGenericType(__armTokenOptionsType));
+ var method = Expression.Call(builder, genMethod, scheme, action);
+ var lambda = Expression.Lambda>(method, builder);
+
+ return lambda.Compile();
+ }
+
+ return builder => builder;
+ }
+ }
+}
diff --git a/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj b/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj
new file mode 100644
index 0000000..d8bb67c
--- /dev/null
+++ b/src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj
@@ -0,0 +1,37 @@
+
+
+
+ DarkLoop.Azure.Functions.Authorization.Isolated
+ DarkLoop.Azure.Functions.Authorization
+ net6.0
+ 0.0.1-preview
+ Azure Functions V4 in Isolated mode extension to enable authentication and authorization on a per function basis based on ASPNET Core frameworks.
+ enable
+
+
+
+ TRACE
+
+
+
+ DEBUG;TRACE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/isolated/Extensions/FunctionContextExtensions.cs b/src/isolated/Extensions/FunctionContextExtensions.cs
new file mode 100644
index 0000000..5d0eff3
--- /dev/null
+++ b/src/isolated/Extensions/FunctionContextExtensions.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Linq;
+using Microsoft.Azure.Functions.Worker;
+
+namespace DarkLoop.Azure.Functions.Authorization.Extensions
+{
+ internal static class FunctionContextExtensions
+ {
+ private const string HttpTriggerBindingType = "httpTrigger";
+
+ ///
+ /// Determines if a function is an HTTP trigger.
+ ///
+ /// The current function context.
+ /// if the function is an HTTP function; otherwise .
+ internal static bool IsHttpTrigger(this FunctionContext context)
+ {
+ return context.FunctionDefinition.InputBindings.Any(b =>
+ b.Value.Type.Equals(HttpTriggerBindingType, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/src/isolated/Features/FunctionsAuthorizationFeature.cs b/src/isolated/Features/FunctionsAuthorizationFeature.cs
new file mode 100644
index 0000000..a390071
--- /dev/null
+++ b/src/isolated/Features/FunctionsAuthorizationFeature.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization.Internal;
+
+namespace DarkLoop.Azure.Functions.Authorization.Features
+{
+ ///
+ /// Marker class for the functions authorization feature.
+ ///
+ internal class FunctionsAuthorizationFeature : IFunctionsAuthorizationFeature
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The function name.
+ public FunctionsAuthorizationFeature(string name)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+
+ Name = name;
+ }
+
+ ///
+ public string Name { get; }
+ }
+}
diff --git a/src/isolated/Features/IFunctionsAuthorizationFeature.cs b/src/isolated/Features/IFunctionsAuthorizationFeature.cs
new file mode 100644
index 0000000..45d3ab1
--- /dev/null
+++ b/src/isolated/Features/IFunctionsAuthorizationFeature.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+namespace DarkLoop.Azure.Functions.Authorization.Features
+{
+ ///
+ /// Marker interface for the functions authorization feature.
+ ///
+ internal interface IFunctionsAuthorizationFeature
+ {
+ ///
+ /// Gets the function name.
+ ///
+ string Name { get; }
+ }
+}
diff --git a/src/isolated/FunctionAuthorizeAttribute.cs b/src/isolated/FunctionAuthorizeAttribute.cs
new file mode 100644
index 0000000..ae5e26f
--- /dev/null
+++ b/src/isolated/FunctionAuthorizeAttribute.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ [DebuggerDisplay("{ToString(),nq}")]
+ public class FunctionAuthorizeAttribute : AuthorizeAttribute, IAuthorizeData
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FunctionAuthorizeAttribute()
+ : base() { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The policy name that determines access to the resource
+ public FunctionAuthorizeAttribute(string policy)
+ : base(policy) { }
+ }
+}
diff --git a/src/isolated/FunctionsAuthorizationExtensionStartup.cs b/src/isolated/FunctionsAuthorizationExtensionStartup.cs
new file mode 100644
index 0000000..e8b0fb2
--- /dev/null
+++ b/src/isolated/FunctionsAuthorizationExtensionStartup.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Metadata;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Extensions.Hosting;
+
+[assembly: WorkerExtensionStartup(typeof(FunctionsAuthorizationExtensionStartup))]
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ public class FunctionsAuthorizationExtensionStartup : WorkerExtensionStartup
+ {
+ public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
+ {
+ applicationBuilder.Services.AddFunctionsAuthorizationCore();
+
+ // This is the only middleware we add in startup as it executes prior to other built-in extensions.
+ // Adding AuthorizationMiddleware at this point removes the ability to access to the request context.
+ // Package consumer is in charge of adding the AuthorizationMiddleware by calling UseFunctionsAuthorization.
+ applicationBuilder.UseMiddleware();
+ }
+ }
+}
diff --git a/src/isolated/FunctionsAuthorizationMiddleware.cs b/src/isolated/FunctionsAuthorizationMiddleware.cs
new file mode 100644
index 0000000..816251d
--- /dev/null
+++ b/src/isolated/FunctionsAuthorizationMiddleware.cs
@@ -0,0 +1,79 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authorization.Policy;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Middleware;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization
+{
+ internal class FunctionsAuthorizationMiddleware : IFunctionsWorkerMiddleware
+ {
+ private readonly IFunctionsAuthorizationProvider _authorizationProvider;
+ private readonly IPolicyEvaluator _policyEvaluator;
+ private readonly IFunctionsAuthorizationResultHandler _authorizationResultHandler;
+ private readonly IOptionsMonitor _configOptions;
+ private readonly ILogger _logger;
+
+ public FunctionsAuthorizationMiddleware(
+ IFunctionsAuthorizationProvider authorizationProvider,
+ IPolicyEvaluator policyEvaluator,
+ IFunctionsAuthorizationResultHandler authorizationHandler,
+ IOptionsMonitor configOptions,
+ ILogger logger)
+ {
+ Check.NotNull(authorizationProvider, nameof(authorizationProvider));
+ Check.NotNull(policyEvaluator, nameof(policyEvaluator));
+ Check.NotNull(authorizationHandler, nameof(authorizationHandler));
+ Check.NotNull(configOptions, nameof(configOptions));
+ Check.NotNull(logger, nameof(logger));
+
+ _authorizationProvider = authorizationProvider;
+ _policyEvaluator = policyEvaluator;
+ _authorizationResultHandler = authorizationHandler;
+ _configOptions = configOptions;
+ _logger = logger;
+ }
+
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ var httpContext = context.GetHttpContext() ??
+ throw new NotSupportedException("DarkLoop Functions authorization is only supported in Isolated mode with ASPNET Core integration.");
+
+ if (this._configOptions.CurrentValue.AuthorizationDisabled)
+ {
+ _logger.LogWarning(
+ $"Authorization through FunctionAuthorizeAttribute is disabled at the application level. Skipping authorization for {httpContext!.Request.GetDisplayUrl()}.");
+
+ await next(context);
+ return;
+ }
+
+ var filter = await _authorizationProvider.GetAuthorizationAsync(context.FunctionDefinition.Name);
+
+ if (filter?.Policy is null)
+ {
+ await next(context);
+ return;
+ }
+
+ var authenticateResult = await _policyEvaluator.AuthenticateAsync(filter.Policy, httpContext);
+
+ if (!filter.AllowAnonymous)
+ {
+ var authorizeResult = await _policyEvaluator.AuthorizeAsync(filter.Policy, authenticateResult, httpContext, httpContext);
+ var authContext = new FunctionAuthorizationContext(
+ context.FunctionDefinition.Name, context, filter.Policy, authorizeResult);
+
+ await _authorizationResultHandler.HandleResultAsync(authContext, httpContext, async (ctx) => await next(ctx));
+ }
+ }
+ }
+}
diff --git a/src/isolated/FunctionsAuthorizationWorkerAppBuilderExtensions.cs b/src/isolated/FunctionsAuthorizationWorkerAppBuilderExtensions.cs
new file mode 100644
index 0000000..ec20163
--- /dev/null
+++ b/src/isolated/FunctionsAuthorizationWorkerAppBuilderExtensions.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Features;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Azure.Functions.Worker
+{
+ ///
+ /// Extension methods for adding the to the application pipeline.
+ ///
+ public static class FunctionsAuthorizationWorkerAppBuilderExtensions
+ {
+ ///
+ /// Adds DarkLoop's Functions authorization middleware to the application pipeline.
+ ///
+ /// The current builder.
+ public static IFunctionsWorkerApplicationBuilder UseFunctionsAuthorization(this IFunctionsWorkerApplicationBuilder builder)
+ {
+ return builder.UseWhen(context =>
+ context.Features.Get() is not null);
+ }
+ }
+}
diff --git a/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs b/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs
new file mode 100644
index 0000000..a75b0be
--- /dev/null
+++ b/src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs
@@ -0,0 +1,76 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using DarkLoop.Azure.Functions.Authorization.Extensions;
+using DarkLoop.Azure.Functions.Authorization.Features;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Middleware;
+using Microsoft.Extensions.Options;
+
+namespace DarkLoop.Azure.Functions.Authorization.Metadata
+{
+ ///
+ /// Classifies functions based on their extension type.
+ ///
+ internal class FunctionsAuthorizationMetadataMiddleware : IFunctionsWorkerMiddleware
+ {
+ private readonly FunctionsAuthorizationOptions _options;
+ private readonly ConcurrentDictionary _trackedHttp = new();
+
+ public FunctionsAuthorizationMetadataMiddleware(
+ IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ ///
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ if (!_trackedHttp.GetOrAdd(context.FunctionId, static (_, c) => c.IsHttpTrigger(), context))
+ {
+ await next(context);
+ return;
+ }
+
+ if(!_options.IsFunctionRegistered(context.FunctionDefinition.Name))
+ {
+ RegisterHttpTriggerAuthorization(context);
+ }
+
+ context.Features.Set(
+ new FunctionsAuthorizationFeature(context.FunctionDefinition.Name));
+
+ await next(context);
+ }
+
+ private void RegisterHttpTriggerAuthorization(FunctionContext context)
+ {
+ var functionName = context.FunctionDefinition.Name;
+ var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch
+ {
+ -1 => string.Empty,
+ var index => context.FunctionDefinition.EntryPoint[..index]
+ };
+
+ var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..];
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false))
+ .FirstOrDefault(t => t is not null)?
+ .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ??
+ throw new MethodAccessException(
+ $"Method instance for function '{context.FunctionDefinition.Name}' " +
+ $"cannot be found or cannot be accessed due to its protection level.");
+
+ var declaringType = method.DeclaringType!;
+
+ _options.RegisterFunctionAuthorizationMetadata(functionName, declaringType, method);
+ }
+ }
+}
diff --git a/src/isolated/README.md b/src/isolated/README.md
new file mode 100644
index 0000000..0ed7d20
--- /dev/null
+++ b/src/isolated/README.md
@@ -0,0 +1,80 @@
+# functions-authorization-isolated
+Bringing AuthorizeAttribute Behavior to Azure Functions v4 in Isolated mode.
+
+It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does.
+
+## Using the package
+### Installing the package
+`dotnet add package DarkLoop.Azure.Functions.Authorization.Isolated`
+
+### Setting up authentication and authorization
+The goal is to utilize the same authentication framework provided for ASP.NET Core
+```csharp
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWebAppliction(builder =>
+ {
+ // Explicitly adding the extension middleware because
+ // registering middleware when extension is loaded does not
+ // place the middleware in the pipeline where required request
+ // information is available.
+ builder.UseFunctionsAuthorization();
+ })
+ .ConfigureServices(services =>
+ {
+ services
+ .AddFunctionsAuthorization()
+ .AddJwtBearer(options =>
+ {
+ options.Authority = "https://login.microsoftonline.com/your-tenant-id";
+ options.Audience = "your-client-id";
+ ...
+ });
+
+ services.AddFunctionsAuthorization(options =>
+ {
+ options.AddPolicy("OnlyAdmins", policy => policy.RequireRole("Admin"));
+ });
+
+ // Add other services
+ })
+ .Build();
+
+host.Run();
+```
+
+Notice the call to `UseFunctionsAuthorization` in the `ConfigureFunctionsWebAppliction` method.
+This is required to ensure that the middleware is placed in the pipeline where required function information is available.`
+
+### Using the attribute
+And now lets use `FunctionAuthorizeAttribute` the same way we use `AuthorizeAttribute` in our ASP.NET Core applications.
+```csharp
+[FunctionAuthorize]
+public class Functions
+{
+ [FunctionName("get-record")]
+ public async Task GetRecord(
+ [HttpTrigger("get")] HttpRequest req, ILogger log)
+ {
+ var user = req.HttpContext.User;
+ var record = GetUserData(user.Identity.Name);
+ return new OkObjectResult(record);
+ }
+
+ [Authorize(Policy = "OnlyAdmins")]
+ [FunctionName("get-all-records")]
+ public async Task GetAllRecords(
+ [HttpTrigger("get")] HttpRequest req, ILogger log)
+ {
+ var records = GetAllData();
+ return new OkObjectResult(records);
+ }
+}
+```
+
+Something really nice to notice is that for Functions in Isolated mode, the `HttpTriggerAttribute` default `AuthenticationLevel` is `Anonymous`, playing really well with the attribute.
+Also notice how the second function uses the `AuthorizeAttribute` attribute to apply a policy to the function. `FunctionAuthorizeAttribute` was left as part of the framework only to make it easier to migrate from In-Proc to Isolated, but they can be used interchangeably.
\ No newline at end of file
diff --git a/src/isolated/Security/FunctionsAuthorizationServiceCollectionExtensions.cs b/src/isolated/Security/FunctionsAuthorizationServiceCollectionExtensions.cs
new file mode 100644
index 0000000..9dfe3d8
--- /dev/null
+++ b/src/isolated/Security/FunctionsAuthorizationServiceCollectionExtensions.cs
@@ -0,0 +1,86 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Internal;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ // Adding this functionality to maintain compatibility with the original library
+ public static class FunctionsAuthorizationServiceCollectionExtensions
+ {
+ ///
+ /// Adds Functions built-in authorization.
+ ///
+ /// The service collection to configure.
+ public static IServiceCollection AddFunctionsAuthorization(this IServiceCollection services)
+ {
+ Check.NotNull(services, nameof(services));
+
+ return services.AddAuthorizationCore();
+ }
+
+ ///
+ /// Adds Functions built-in authorization handlers and allows for further configuration.
+ ///
+ /// The service collection to configure.
+ /// The method to configure the authorization options.
+ public static IServiceCollection AddFunctionsAuthorization(
+ this IServiceCollection services, Action configure)
+ {
+ Check.NotNull(services, nameof(services));
+ Check.NotNull(configure, nameof(configure));
+
+ return services.AddAuthorizationCore(configure);
+ }
+
+ ///
+ /// Adds Functions built-in authentication.
+ ///
+ public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, string? defaultScheme = null)
+ {
+ Check.NotNull(services, nameof(services));
+
+ return services.AddFunctionsAuthentication(defaultScheme, null);
+ }
+
+ ///
+ /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication.
+ ///
+ /// The configuration logic.
+ public static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, Action? configure)
+ {
+ Check.NotNull(services, nameof(services));
+
+ return services.AddFunctionsAuthentication(null, configure);
+ }
+
+ private static FunctionsAuthenticationBuilder AddFunctionsAuthentication(
+ this IServiceCollection services, string? defaultScheme, Action? configure)
+ {
+ var builder = new FunctionsAuthenticationBuilder(services);
+
+ if (!string.IsNullOrWhiteSpace(defaultScheme))
+ {
+ services.AddAuthentication(defaultScheme);
+ }
+ else if (configure is not null)
+ {
+ services.AddAuthentication(configure);
+ }
+ else
+ {
+ services.AddAuthentication();
+ }
+
+ return builder;
+
+ }
+ }
+}
diff --git a/test/Abstractions.Tests/Abstractions.Tests.csproj b/test/Abstractions.Tests/Abstractions.Tests.csproj
new file mode 100644
index 0000000..f7769da
--- /dev/null
+++ b/test/Abstractions.Tests/Abstractions.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Abstractions.Tests/Fakes/AuthorizeDataFake.cs b/test/Abstractions.Tests/Fakes/AuthorizeDataFake.cs
new file mode 100644
index 0000000..b6df1bf
--- /dev/null
+++ b/test/Abstractions.Tests/Fakes/AuthorizeDataFake.cs
@@ -0,0 +1,17 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Abstractions.Tests.Fakes
+{
+ [ExcludeFromCodeCoverage]
+ internal class AuthorizeDataFake : IAuthorizeData
+ {
+ public string? Policy { get; set; }
+ public string? Roles { get; set; }
+ public string? AuthenticationSchemes { get; set; }
+ }
+}
diff --git a/test/Abstractions.Tests/FunctionAuthorizationMetadataCollectionTests.cs b/test/Abstractions.Tests/FunctionAuthorizationMetadataCollectionTests.cs
new file mode 100644
index 0000000..ada3b9f
--- /dev/null
+++ b/test/Abstractions.Tests/FunctionAuthorizationMetadataCollectionTests.cs
@@ -0,0 +1,139 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using Abstractions.Tests.Fakes;
+using DarkLoop.Azure.Functions.Authorization;
+
+namespace Abstractions.Tests
+{
+ [TestClass]
+ public class FunctionAuthorizationMetadataCollectionTests
+ {
+ [TestMethod("MetadataCollection: should return same instance for same type")]
+ public void MetadataCollectionShouldReturnSameInstanceForSameType()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection.Add(this.GetType(), out var existsFirst);
+ var metadata2 = collection.Add(this.GetType(), out var existsSecond);
+
+ // Assert
+ Assert.AreSame(metadata1, metadata2);
+ Assert.IsFalse(existsFirst);
+ Assert.IsTrue(existsSecond);
+ }
+
+ [TestMethod("MetadataCollection: should return different instance for different type")]
+ public void MetadataCollectionShouldReturnDifferentInstanceForDifferentType()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection.Add(this.GetType(), out var existsFirst);
+ var metadata2 = collection.Add(typeof(FunctionAuthorizationMetadataCollection), out var existsSecond);
+
+ // Assert
+ Assert.AreNotSame(metadata1, metadata2);
+ Assert.IsFalse(existsFirst);
+ Assert.IsFalse(existsSecond);
+ }
+
+ [TestMethod("MetadataCollection: should return same instance for same function and type")]
+ public void MetadataCollectionShouldReturnSameInstanceForSameFunctionAndType()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection.Add("TestFunction", this.GetType());
+ var metadata2 = collection.Add("TestFunction", this.GetType());
+
+ // Assert
+ Assert.AreSame(metadata1, metadata2);
+ Assert.AreEqual(metadata1.AuthorizationId, metadata2.AuthorizationId);
+ }
+
+ [TestMethod("MetadataCollection: should return different instance for different function and type")]
+ public void MetadataCollectionShouldReturnDifferentInstanceForDifferentFunctionAndType()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection.Add("TestFunction", this.GetType());
+ var metadata2 = collection.Add("TestFunction", typeof(FunctionAuthorizationMetadataCollection));
+
+ // Assert
+ Assert.AreNotSame(metadata1, metadata2);
+ Assert.AreNotEqual(metadata1.AuthorizationId, metadata2.AuthorizationId);
+ }
+
+ [TestMethod("MetadataCollection: should aggregate type and function metadata")]
+ public void MetadataCollectionShouldAggregateTypeAndFunctionMetadata()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection
+ .Add(this.GetType(), out _)
+ .AddAuthorizeData(new AuthorizeDataFake{ Policy = "Policy1" });
+
+ var metadata2 = collection
+ .Add("TestFunction", this.GetType())
+ .AddAuthorizeData(new AuthorizeDataFake{ Policy = "Policy2"})
+ .AllowAnonymousAccess();
+
+ var single = collection
+ .GetMetadata("TestFunction", this.GetType());
+
+ // Assert
+ Assert.AreNotSame(metadata1, metadata2);
+ Assert.AreNotSame(metadata2, single);
+ Assert.AreEqual(metadata2.AuthorizationId, single.AuthorizationId);
+ Assert.AreEqual(2, single.AuthorizationData.Count);
+
+ Assert.AreSame("Policy1", single.AuthorizationData[0].Policy);
+ Assert.AreSame("Policy2", single.AuthorizationData[1].Policy);
+ Assert.IsTrue(single.AllowsAnonymousAccess);
+ }
+
+ [TestMethod("MetadataCollection: AllowAnonymousAccess on type should inherit to function")]
+ public void MetadataCollectionAllowAnonymousAccessOnTypeShouldInheritToFunction()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata1 = collection
+ .Add(this.GetType(), out _)
+ .AllowAnonymousAccess();
+
+ var metadata2 = collection
+ .GetMetadata("TestFunction", this.GetType());
+
+ // Assert
+ Assert.AreNotSame(metadata1, metadata2);
+ Assert.IsTrue(metadata2.AllowsAnonymousAccess);
+ }
+
+ [TestMethod("MetadataCollection: should return metadata for unregistered function")]
+ public void MetadataCollectionShouldReturnEmptyMetadataForUnregisteredFunction()
+ {
+ // Arrange
+ var collection = new FunctionAuthorizationMetadataCollection();
+
+ // Act
+ var metadata = collection.GetMetadata("TestFunction", this.GetType());
+
+ // Assert
+ Assert.IsNotNull(metadata);
+ Assert.AreEqual(0, metadata.AuthorizationData.Count);
+ Assert.IsFalse(metadata.AllowsAnonymousAccess);
+ }
+ }
+}
diff --git a/test/Abstractions.Tests/FunctionAuthorizationMetadataTests.cs b/test/Abstractions.Tests/FunctionAuthorizationMetadataTests.cs
new file mode 100644
index 0000000..f97587b
--- /dev/null
+++ b/test/Abstractions.Tests/FunctionAuthorizationMetadataTests.cs
@@ -0,0 +1,36 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Abstractions.Tests.Fakes;
+using DarkLoop.Azure.Functions.Authorization;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Abstractions.Tests
+{
+ [TestClass]
+ public class FunctionAuthorizationMetadataTests
+ {
+ [TestMethod("Metadata: should return the same number of authorization data elements it received")]
+ public void Metadata_ShouldReturnTheSameNumberOfAuthorizationDataElementsItReceived()
+ {
+ // Arrange
+ var metadata = new FunctionAuthorizationMetadata("TestFunction", this.GetType());
+
+ // Act
+ metadata
+ .AddAuthorizeData(new AuthorizeDataFake())
+ .AddAuthorizeData(new[] { new AuthorizeDataFake(), new AuthorizeDataFake() });
+
+
+ // Assert
+ Assert.AreEqual(3, metadata.AuthorizationData.Count);
+ }
+ }
+}
diff --git a/test/Abstractions.Tests/FunctionsAuthorizationFilterCacheTests.cs b/test/Abstractions.Tests/FunctionsAuthorizationFilterCacheTests.cs
new file mode 100644
index 0000000..da24c22
--- /dev/null
+++ b/test/Abstractions.Tests/FunctionsAuthorizationFilterCacheTests.cs
@@ -0,0 +1,36 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+using DarkLoop.Azure.Functions.Authorization;
+using DarkLoop.Azure.Functions.Authorization.Cache;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+
+namespace Abstractions.Tests
+{
+ [TestClass]
+ public class FunctionsAuthorizationFilterCacheTests
+ {
+ [TestMethod("FilterCache: SetFilter should not replace existing instance")]
+ public void SetFilterShouldNotReplaceExisting()
+ {
+ // Arrange
+ var cache = new FunctionsAuthorizationFilterCache();
+ var filter = new FunctionAuthorizationFilter(null, true);
+
+ // Act
+ cache.SetFilter(1, filter);
+ cache.SetFilter(1, new FunctionAuthorizationFilter(
+ new AuthorizationPolicy(
+ new[] { new DenyAnonymousAuthorizationRequirement() },
+ new[] { JwtBearerDefaults.AuthenticationScheme })));
+
+ // Assert
+ cache.TryGetFilter(1, out var extractedFilter);
+ Assert.AreSame(filter, extractedFilter);
+ Assert.AreSame(null, filter.Policy);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Abstractions.Tests/Usings.cs b/test/Abstractions.Tests/Usings.cs
new file mode 100644
index 0000000..423ac02
--- /dev/null
+++ b/test/Abstractions.Tests/Usings.cs
@@ -0,0 +1,5 @@
+//
+// Copyright (c) DarkLoop. All rights reserved.
+//
+
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
\ No newline at end of file
diff --git a/test/DarkLoop.Azure.Functions.Authorize.Tests/DarkLoop.Azure.Functions.Authorize.Tests.csproj b/test/DarkLoop.Azure.Functions.Authorize.Tests/DarkLoop.Azure.Functions.Authorize.Tests.csproj
deleted file mode 100644
index fe55a24..0000000
--- a/test/DarkLoop.Azure.Functions.Authorize.Tests/DarkLoop.Azure.Functions.Authorize.Tests.csproj
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- net7.0
- false
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test/DarkLoop.Azure.Functions.Authorize.Tests/FunctionsHttpAuthorizationHandlerShould.cs b/test/DarkLoop.Azure.Functions.Authorize.Tests/FunctionsHttpAuthorizationHandlerShould.cs
deleted file mode 100644
index 3830ab1..0000000
--- a/test/DarkLoop.Azure.Functions.Authorize.Tests/FunctionsHttpAuthorizationHandlerShould.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Threading.Tasks;
-using DarkLoop.Azure.Functions.Authorize.Filters;
-using DarkLoop.Azure.Functions.Authorize.Security;
-using DarkLoop.Azure.Functions.Authorize.Tests.Mocks;
-using FluentAssertions;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Azure.WebJobs.Host;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Moq;
-
-namespace DarkLoop.Azure.Functions.Authorize.Tests
-{
- [TestClass]
- public class FunctionsHttpAuthorizationHandlerShould
- {
- [TestMethod]
- public void ThrowExceptionWhenFiltersIndexIsNull()
- {
- var action = new Action(() => new FunctionsHttpAuthorizationHandler(null));
- action.Should().Throw("No null param is allowed");
- }
-
- [TestMethod]
- public void NotThrowExceptionWhenFiltersIndexIsNotNull()
- {
- var index = new Mock();
- var action = new Action(() => new FunctionsHttpAuthorizationHandler(index.Object));
- action.Should().NotThrow("index is expected");
- }
-
- [TestMethod]
- public void ThrowWhenOnAuthorizingFunctionContextParamIsNull()
- {
- var index = new Mock();
- var handler = new FunctionsHttpAuthorizationHandler(index.Object);
- var action = new Func(async () => await handler.OnAuthorizingFunctionInstance(null, null));
- action.Should().Throw("functionContext is expected not to be null");
- }
- }
-}
diff --git a/test/DarkLoop.Azure.Functions.Authorize.Tests/Mocks/FunctionExecutingContextMock.cs b/test/DarkLoop.Azure.Functions.Authorize.Tests/Mocks/FunctionExecutingContextMock.cs
deleted file mode 100644
index d5f9992..0000000
--- a/test/DarkLoop.Azure.Functions.Authorize.Tests/Mocks/FunctionExecutingContextMock.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Microsoft.Azure.WebJobs.Host;
-using Microsoft.Extensions.Logging;
-using Moq;
-
-namespace DarkLoop.Azure.Functions.Authorize.Tests.Mocks
-{
- class FunctionExecutingContextMock : Mock
- {
- public FunctionExecutingContextMock(
- IReadOnlyDictionary arguments,
- IDictionary properties, Guid functionInstanceId, string functionName, ILogger logger)
- : base(arguments, properties, functionInstanceId, functionName, logger)
- {
-
- }
- }
-}