├── .assets └── icon.png ├── .build └── release.props ├── .editorconfig ├── .gitignore ├── ChangeLog.md ├── Functions-Authorize.sln ├── LICENSE ├── NuGet.Config ├── README.md ├── sample ├── SampleInProcFunctions.V4 │ ├── .gitignore │ ├── HelperFunctions.cs │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── SampleInProcFunctions.V4.csproj │ ├── Startup.cs │ ├── TestFunction.cs │ └── host.json ├── SampleIsolatedFunctions.V4 │ ├── .gitignore │ ├── HelperFunctions.cs │ ├── Program.cs │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── SampleIsolatedFunctions.V4.csproj │ ├── TestFunction.cs │ └── host.json └── SampleIsolatedFunctionsFSharp.V4 │ ├── .vscode │ └── extensions.json │ ├── HelperFunctions.fs │ ├── Program.fs │ ├── Properties │ ├── serviceDependencies.json │ └── serviceDependencies.local.json │ ├── SampleIsolatedFunctionsFSharp_V4.fsproj │ ├── TestFunction.fs │ ├── host.json │ └── local.settings.json ├── src ├── abstractions │ ├── Cache │ │ ├── FunctionsAuthorizationFilterCache.cs │ │ └── IFunctionsAuthorizationFilterCache.cs │ ├── Constants.cs │ ├── DarkLoop.Azure.Functions.Authorization.Abstractions.csproj │ ├── EmptySchemeStrategy.cs │ ├── FunctionAuthorizationContext.cs │ ├── FunctionAuthorizationFeature.cs │ ├── FunctionAuthorizationFilter.cs │ ├── FunctionAuthorizationMetadata.cs │ ├── FunctionAuthorizationMetadataCollection.cs │ ├── FunctionAuthorizationTypeMap.cs │ ├── FunctionsAuthenticationBuilder.cs │ ├── FunctionsAuthenticationBuilderExtensions.cs │ ├── FunctionsAuthorizationBuilder.cs │ ├── FunctionsAuthorizationCoreServiceCollectionExtensions.cs │ ├── FunctionsAuthorizationOptions.cs │ ├── FunctionsAuthorizationProvider.cs │ ├── FunctionsAuthorizationResultHandler.cs │ ├── IFunctionsAuthorizationProvider.cs │ ├── IFunctionsAuthorizationResultHandler.cs │ ├── Internal │ │ ├── Check.cs │ │ ├── FunctionsAuthorizationOptionsExtensions.cs │ │ ├── FunctionsFeatureCollectionExtension.cs │ │ └── KeyedMonitor.cs │ ├── JwtFunctionsBearerDefaults.cs │ ├── Properties │ │ ├── Messages.Designer.cs │ │ └── Messages.resx │ ├── README.md │ └── Security │ │ └── AuthorizationBuilderExtensions.cs ├── in-proc │ ├── .gitignore │ ├── Bindings │ │ └── FunctionsAuthorizeBindingProvider.cs │ ├── DarkLoop.Azure.Functions.Authorization.InProcess.csproj │ ├── FunctionAuthorizationContextInternal.cs │ ├── FunctionAuthorizationException.cs │ ├── FunctionAuthorizeAttribute.cs │ ├── FunctionExecutingContextExtensions.cs │ ├── FunctionsAuthExtension.cs │ ├── FunctionsAuthorizationExecutor.cs │ ├── FunctionsAuthorizationHostBuilderExtensions.cs │ ├── FunctionsAuthorizeStartup.cs │ ├── IFunctionsAuthorizationExecutor.cs │ ├── Properties │ │ ├── Strings.Designer.cs │ │ ├── Strings.resx │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── README.md │ ├── Security │ │ ├── FunctionsAuthenticationBuilderExtensions.cs │ │ ├── FunctionsAuthenticationServiceCollectionExtensions.cs │ │ └── FunctionsAuthorizationServiceCollectionExtensions.cs │ └── Utils │ │ └── HostUtils.cs └── isolated │ ├── DarkLoop.Azure.Functions.Authorization.Isolated.csproj │ ├── Extensions │ └── FunctionContextExtensions.cs │ ├── Features │ ├── FunctionsAuthorizationFeature.cs │ └── IFunctionsAuthorizationFeature.cs │ ├── FunctionAuthorizeAttribute.cs │ ├── FunctionsAuthorizationExtensionStartup.cs │ ├── FunctionsAuthorizationMiddleware.cs │ ├── FunctionsAuthorizationServiceCollectionExtensions.cs │ ├── FunctionsAuthorizationWorkerAppBuilderExtensions.cs │ ├── Metadata │ └── FunctionsAuthorizationMetadataMiddleware.cs │ ├── Properties │ ├── IsolatedMessages.Designer.cs │ └── IsolatedMessages.resx │ └── README.md └── test ├── Abstractions.Tests ├── Abstractions.Tests.csproj ├── Fakes │ └── AuthorizeDataFake.cs ├── FunctionAuthorizationMetadataCollectionTests.cs ├── FunctionAuthorizationMetadataTests.cs ├── FunctionAuthorizationTypeMapTests.cs ├── FunctionsAuthorizationFilterCacheTests.cs ├── FunctionsAuthorizationProviderTests.cs ├── FunctionsAuthorizationResultHandlerTests.cs ├── Internal │ └── KeyedMonitorTests.cs └── Usings.cs ├── Common.Tests ├── Common.Tests.csproj ├── HttpUtils.cs ├── JwtUtils.cs ├── LoggerUtils.cs └── TestTokenValidator.cs ├── InProc.Tests ├── FunctionsAuthorizationExecutorTests.cs ├── GlobalUsings.cs └── InProc.Tests.csproj └── Isolated.Tests ├── ConcurrentTests.cs ├── Fakes ├── FakeFunctionClass.cs └── FakeInvocationFeatures.cs ├── Features └── FunctionsAuthorizationFeatureTests.cs ├── FunctionContextExtensionsTests.cs ├── FunctionsAuthorizationMiddlewareTests.cs ├── GlobalUsings.cs ├── Isolated.Tests.csproj └── Metadata └── FunctionsAuthorizationMetadataMiddlewareTests.cs /.assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dark-loop/functions-authorize/6d3e390231c1778c037b4ce71c68273c607684e9/.assets/icon.png -------------------------------------------------------------------------------- /.build/release.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(AssmeblyName) 5 | DarkLoop 6 | DarkLoop 7 | DarkLoop - All rights reserved 8 | DarkLoop's Azure Functions Authorization 9 | false 10 | 4.0.0.0 11 | 4.2.0 12 | $(Version).0 13 | https://github.com/dark-loop/functions-authorize 14 | https://github.com/dark-loop/functions-authorize/blob/master/LICENSE 15 | Git 16 | AuthorizeAttribute, Authorize, Azure Functions, Azure, Bearer, JWT, Policy based authorization 17 | icons/icon.png 18 | https://en.gravatar.com/userimage/22176525/45f25acea686a783e5b2ca172d72db71.png 19 | true 20 | ../dl-sftwr-sn-key.snk 21 | 0024000004800000940000000602000000240000525341310004000001000100791e7f618a12452d7ced5310f6203d0d227f9d26b146555e7e67a1801695dcf7c552421620a662f54b072f7be1efa885c074d4b9c76a4d6d154721d1c3b1f39164cfaf9ebdf9b7672ff320c89c5a64c90e25330f90a12bf42a1c57b70523e785167dbbfb7a0fdc9eb8d15112f758b89bab51953b08cfb2218095bc45171c99c5 22 | true 23 | true 24 | README.md 25 | 26 | 27 | 28 | $(BuildNumber.Substring($([MSBuild]::Add($(BuildNumber.LastIndexOf('.')), 1)))) 29 | $([System.DateTime]::Now.ToString('yyMMdd')) 30 | $(DateNumber)-$(Revision) 31 | -preview-$(BuildIDNumber) 32 | $(Version)$(PreviewVersion) 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | file_header_template = \n Copyright (c) DarkLoop. All rights reserved.\n 4 | 5 | # CS0618: Type or member is obsolete 6 | dotnet_diagnostic.CS0618.severity = silent 7 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # functions-authorize 2 | Extension bringing AuthorizeAttribute Behavior to Azure Functions In-Proc and Isolated mode. For the latter is only available with ASPNET Core integration. 3 | 4 | It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does. 5 | 6 | > **Breaking for current package consumers**
7 | > Starting with version 4.1.0, due to security changes made on the Functions runtime, the Bearer scheme is no longer supported for your app functions.
8 | > Use `AddJwtFunctionsBearer(Action)` instead of `AddJwtBearer(Action)` when setting up authentication. 9 | Using `AddJwtBearer` will generate a compilation error when used against `FunctionsAuthenticationBuilder`. 10 | We are introducing `JwtFunctionsBearerDefaults` to refer to the suggested new custom scheme name.
11 | No changes should be required if already using a custom scheme name.
12 | > Refer to respective README documentation for isolated and in-process for more information. 13 | 14 | ## Getting Started 15 | - [Azure Functions V3+ In-Proc mode](./src/in-proc/README.md) 16 | - [Azure Functions V4 Isolated mode with ASPNET Core integration](./src/isolated/README.md) 17 | 18 | ## License 19 | 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. 20 | 21 | ## Package Status 22 | ### Releases 23 | [![Nuget](https://img.shields.io/nuget/v/DarkLoop.Azure.Functions.Authorization.Abstractions.svg)](https://www.nuget.org/packages/DarkLoop.Azure.Functions.Authorization.Abstractions) 24 | 25 | ### Builds 26 | ![master build status](https://dev.azure.com/darkloop/DarkLoop%20Core%20Library/_apis/build/status/Open%20Source/Functions%20Authorize%20-%20Pack?branchName=master) 27 | 28 | ## Change Log 29 | You can access the change log [here](https://github.com/dark-loop/functions-authorize/blob/master/ChangeLog.md). -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/HelperFunctions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Security.Claims; 7 | using System.Threading.Tasks; 8 | using Common.Tests; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Extensions.Http; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace SampleInProcFunctions.V4 16 | { 17 | public static class HelperFunctions 18 | { 19 | [FunctionName("GetTestToken")] 20 | public static async Task Run( 21 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, 22 | ILogger log) 23 | { 24 | var firstName = "Test"; 25 | var lastName = "User"; 26 | var email = "test.user@domain.com"; 27 | var token = JwtUtils.GenerateJwtToken(new[] { 28 | new Claim("aud", "api://default"), 29 | new Claim("iss", "https://localhost/jwt/"), 30 | new Claim("scp", "user_impersonation"), 31 | new Claim("tid", Guid.NewGuid().ToString()), 32 | new Claim("oid", Guid.NewGuid().ToString()), 33 | new Claim("name", $"{firstName} {lastName}"), 34 | new Claim(ClaimTypes.Name, email), 35 | new Claim(ClaimTypes.Upn, email), 36 | new Claim(ClaimTypes.Email, email), 37 | new Claim(ClaimTypes.GivenName, firstName), 38 | new Claim(ClaimTypes.Surname, lastName), 39 | new Claim("role", "Just a user"), 40 | new Claim("role", "another user"), 41 | }); 42 | 43 | return await Task.FromResult(new OkObjectResult(token)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/SampleInProcFunctions.V4.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | v4 5 | 51dc0b9d-8e74-45ec-aebc-1d3d6934faf5 6 | false 7 | 8 | 9 | 10 | <_FunctionsSkipCleanOutput>true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | PreserveNewest 35 | Never 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Common.Tests; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4; 8 | using Microsoft.AspNetCore.Authentication.JwtBearer; 9 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.IdentityModel.Tokens; 13 | 14 | [assembly: FunctionsStartup(typeof(Startup))] 15 | 16 | namespace DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4 17 | { 18 | class Startup : FunctionsStartup 19 | { 20 | public IConfigurationRoot Configuration { get; private set; } 21 | 22 | public override void Configure(IFunctionsHostBuilder builder) 23 | { 24 | builder.Services 25 | .AddFunctionsAuthentication(options => 26 | { 27 | options.DefaultScheme = JwtFunctionsBearerDefaults.AuthenticationScheme; 28 | options.DefaultAuthenticateScheme = JwtFunctionsBearerDefaults.AuthenticationScheme; 29 | options.DefaultChallengeScheme = JwtFunctionsBearerDefaults.AuthenticationScheme; 30 | }) 31 | .AddJwtFunctionsBearer(options => 32 | { 33 | // this line is here to bypass the token validation 34 | // and test the functionality of this library. 35 | // you can create a dummy token by executing the GetTestToken function in HelperFunctions.cs 36 | // THE FOLLOWING LINE SHOULD BE REMOVED IN A REAL-WORLD SCENARIO 37 | options.SecurityTokenValidators.Add(new TestTokenValidator()); 38 | 39 | // this is what you should look for in a real-world scenario 40 | // comment the lines if you cloned this repository and want to test the library 41 | //options.Authority = "https://login.microsoftonline.com/"; 42 | //options.Audience = ""; 43 | //options.TokenValidationParameters = new TokenValidationParameters 44 | //{ 45 | // ValidateIssuer = true, 46 | // ValidateAudience = true, 47 | // ValidateLifetime = true, 48 | // ValidateIssuerSigningKey = true, 49 | //}; 50 | }); 51 | 52 | builder.Services.AddFunctionsAuthorization(options => 53 | { 54 | // Add your policies here 55 | }); 56 | 57 | // If you want to disable authorization for all functions 58 | // decorated with FunctionAuthorizeAttribute you can add the following configuration. 59 | // If you bind it to configuration, you can modify the setting remotely using 60 | // Azure App Configuration or other configuration providers without the need to restart app. 61 | if (builder.IsLocalAuthorizationContext()) 62 | { 63 | builder.Services.Configure(Configuration.GetSection("AuthOptions")); 64 | } 65 | } 66 | 67 | public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) 68 | { 69 | builder.ConfigurationBuilder.AddUserSecrets(true, reloadOnChange: true); 70 | 71 | Configuration = builder.ConfigurationBuilder.Build(); 72 | 73 | base.ConfigureAppConfiguration(builder); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/TestFunction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Azure.WebJobs; 11 | using Microsoft.Azure.WebJobs.Extensions.Http; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace DarkLoop.Azure.Functions.Authorize.SampleFunctions.V4 16 | { 17 | public static class TestFunction 18 | { 19 | [FunctionName("TestFunction")] 20 | [FunctionAuthorize] 21 | public static async Task Run( 22 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, 23 | ILogger log) 24 | { 25 | log.LogInformation("C# HTTP trigger function processed a request."); 26 | 27 | var provider = req.HttpContext.RequestServices; 28 | var schProvider = provider.GetService(); 29 | 30 | var sb = new StringBuilder(); 31 | sb.AppendLine("Authentication schemes:"); 32 | 33 | if (schProvider is not null) 34 | { 35 | foreach (var scheme in await schProvider.GetAllSchemesAsync()) 36 | sb.AppendLine($" {scheme.Name} -> {scheme.HandlerType}"); 37 | } 38 | 39 | sb.AppendLine(); 40 | sb.AppendLine($"User:"); 41 | sb.AppendLine($" Name -> {req.HttpContext.User.Identity!.Name}"); 42 | sb.AppendLine($" Email -> {req.HttpContext.User.FindFirst("email")?.Value}"); 43 | 44 | return new OkObjectResult(sb.ToString()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/SampleInProcFunctions.V4/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | }, 10 | "logLevel": { 11 | "Darkloop": "Information", 12 | "Microsoft": "Information" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | *.arm.json 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | #*.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | # The packages folder can be ignored because of Package Restore 161 | **/packages/* 162 | # except build/, which is used as an MSBuild target. 163 | !**/packages/build/ 164 | # Uncomment if necessary however generally it will be regenerated when needed 165 | #!**/packages/repositories.config 166 | # NuGet v3's project.json files produces more ignoreable files 167 | *.nuget.props 168 | *.nuget.targets 169 | 170 | # Microsoft Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Microsoft Azure Emulator 175 | ecf/ 176 | rcf/ 177 | 178 | # Windows Store app package directories and files 179 | AppPackages/ 180 | BundleArtifacts/ 181 | Package.StoreAssociation.xml 182 | _pkginfo.txt 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.jfm 197 | *.pfx 198 | *.publishsettings 199 | node_modules/ 200 | orleans.codegen.cs 201 | 202 | # Since there are multiple workflows, uncomment next line to ignore bower_components 203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 204 | #bower_components/ 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | paket-files/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | 260 | # CodeRush 261 | .cr/ 262 | 263 | # Python Tools for Visual Studio (PTVS) 264 | __pycache__/ 265 | *.pyc -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/HelperFunctions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Security.Claims; 6 | using Common.Tests; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Azure.Functions.Worker; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace SampleInProcFunctions.V4 13 | { 14 | public static class HelperFunctions 15 | { 16 | [Function("GetTestToken")] 17 | public static async Task Run( 18 | [HttpTrigger("get", Route = null)] HttpRequest req, 19 | ILogger log) 20 | { 21 | var firstName = "Test"; 22 | var lastName = "User"; 23 | var email = "test.user@domain.com"; 24 | var token = JwtUtils.GenerateJwtToken(new[] { 25 | new Claim("aud", "api://default"), 26 | new Claim("iss", "https://localhost/jwt/"), 27 | new Claim("scp", "user_impersonation"), 28 | new Claim("tid", Guid.NewGuid().ToString()), 29 | new Claim("oid", Guid.NewGuid().ToString()), 30 | new Claim("name", $"{firstName} {lastName}"), 31 | new Claim(ClaimTypes.Name, email), 32 | new Claim(ClaimTypes.Upn, email), 33 | new Claim(ClaimTypes.Email, email), 34 | new Claim(ClaimTypes.GivenName, firstName), 35 | new Claim(ClaimTypes.Surname, lastName), 36 | new Claim("role", "Just a user"), 37 | new Claim("role", "admin"), 38 | }); 39 | 40 | return await Task.FromResult(new OkObjectResult(token)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Common.Tests; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using Microsoft.Azure.Functions.Worker; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | // IMPORTANT: because local.settings.json is not included in the repository, you must create it manually 13 | // If you don't create it. the isolated function will not run. Ensure that the file has the following content: 14 | // 15 | // { 16 | // "IsEncrypted": false, 17 | // "Values": { 18 | // "AzureWebJobsStorage": "UseDevelopmentStorage=true", 19 | // "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" 20 | // } 21 | // } 22 | 23 | var host = new HostBuilder() 24 | .ConfigureFunctionsWebApplication(builder => 25 | { 26 | builder.UseFunctionsAuthorization(); 27 | }) 28 | .ConfigureServices(services => 29 | { 30 | services 31 | .AddFunctionsAuthentication(JwtFunctionsBearerDefaults.AuthenticationScheme) 32 | .AddJwtFunctionsBearer(options => 33 | { 34 | // this line is here to bypass the token validation 35 | // and test the functionality of this library. 36 | // you can create a dummy token by executing the GetTestToken function in HelperFunctions.cs 37 | // THE FOLLOWING LINE SHOULD BE REMOVED IN A REAL-WORLD SCENARIO 38 | options.SecurityTokenValidators.Add(new TestTokenValidator()); 39 | 40 | // this is what you should look for in a real-world scenario 41 | // comment the lines if you cloned this repository and want to test the library 42 | options.Authority = "https://login.microsoftonline.com/"; 43 | options.Audience = ""; 44 | options.TokenValidationParameters = new TokenValidationParameters 45 | { 46 | ValidateIssuer = true, 47 | ValidateAudience = true, 48 | ValidateLifetime = true, 49 | ValidateIssuerSigningKey = true, 50 | }; 51 | }); 52 | 53 | services 54 | .AddFunctionsAuthorization(options => 55 | { 56 | // Add your policies here 57 | }); 58 | }) 59 | .Build(); 60 | 61 | host.Run(); 62 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/SampleIsolatedFunctions.V4.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/TestFunction.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Text; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Azure.Functions.Worker; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace SampleIsolatedFunctions.V4 16 | { 17 | [FunctionAuthorize(AuthenticationSchemes = "FunctionsBearer")] 18 | public class TestFunction 19 | { 20 | private readonly ILogger _logger; 21 | 22 | public TestFunction(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | [Function("TestFunction")] 28 | [Authorize(Roles = "admin")] 29 | public async Task Run([HttpTrigger("get", "post")] HttpRequest req) 30 | { 31 | _logger.LogInformation("C# HTTP trigger function processed a request."); 32 | 33 | var provider = req.HttpContext.RequestServices; 34 | var schProvider = provider.GetService(); 35 | 36 | var sb = new StringBuilder(); 37 | sb.AppendLine("Authentication schemes:"); 38 | 39 | if (schProvider is not null) 40 | { 41 | foreach (var scheme in await schProvider.GetAllSchemesAsync()) 42 | sb.AppendLine($" {scheme.Name} -> {scheme.HandlerType}"); 43 | } 44 | 45 | sb.AppendLine(); 46 | sb.AppendLine($"User:"); 47 | sb.AppendLine($" Name -> {req.HttpContext.User.Identity!.Name}"); 48 | sb.AppendLine($" Email -> {req.HttpContext.User.FindFirst("email")?.Value}"); 49 | 50 | return new OkObjectResult(sb.ToString()); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctions.V4/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/HelperFunctions.fs: -------------------------------------------------------------------------------- 1 | namespace SampleInProcFunctions.V4 2 | 3 | open System.Security.Claims 4 | open Common.Tests 5 | open Microsoft.AspNetCore.Http 6 | open Microsoft.AspNetCore.Mvc 7 | open Microsoft.Azure.Functions.Worker 8 | open Microsoft.Extensions.Logging 9 | open System 10 | 11 | type HelperFunctions() = 12 | 13 | [] 14 | member _.Run( 15 | [] 16 | req: HttpRequest, 17 | log: ILogger) = 18 | task { 19 | 20 | let firstName = "Test" 21 | let lastName = "User" 22 | let email = "test.user@domain.com" 23 | let token = JwtUtils.GenerateJwtToken( 24 | [ 25 | new Claim("aud", "api://default") 26 | new Claim("iss", "https://localhost/jwt/") 27 | new Claim("scp", "user_impersonation") 28 | new Claim("tid", Guid.NewGuid().ToString()) 29 | new Claim("oid", Guid.NewGuid().ToString()) 30 | new Claim("name", $"{firstName} {lastName}") 31 | new Claim(ClaimTypes.Name, email) 32 | new Claim(ClaimTypes.Upn, email) 33 | new Claim(ClaimTypes.Email, email) 34 | new Claim(ClaimTypes.GivenName, firstName) 35 | new Claim(ClaimTypes.Surname, lastName) 36 | new Claim("role", "Just a user") 37 | new Claim("role", "admin") 38 | ]) 39 | 40 | return OkObjectResult(token) 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | open Common.Tests 4 | open DarkLoop.Azure.Functions.Authorization 5 | open Microsoft.Azure.Functions.Worker 6 | open Microsoft.Extensions.DependencyInjection 7 | open Microsoft.Extensions.Hosting 8 | open Microsoft.IdentityModel.Tokens 9 | 10 | // IMPORTANT: because local.settings.json is not included in the repository, you must create it manually 11 | // If you don't create it. the isolated function will not run. Ensure that the file has the following content: 12 | // 13 | // { 14 | // "IsEncrypted": false, 15 | // "Values": { 16 | // "AzureWebJobsStorage": "UseDevelopmentStorage=true", 17 | // "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" 18 | // } 19 | // } 20 | 21 | let host = 22 | HostBuilder() 23 | .ConfigureFunctionsWebApplication(fun builder -> 24 | //This is needed to make F# variants of startup work nicely 25 | FunctionsAuthorizationExtensionStartup().Configure(builder) 26 | builder.UseFunctionsAuthorization() |> ignore ) 27 | .ConfigureServices(fun services -> 28 | services 29 | .AddFunctionsAuthentication(JwtFunctionsBearerDefaults.AuthenticationScheme) 30 | .AddJwtFunctionsBearer(fun options -> 31 | // this line is here to bypass the token validation 32 | // and test the functionality of this library. 33 | // you can create a dummy token by executing the GetTestToken function in HelperFunctions.cs 34 | // THE FOLLOWING LINE SHOULD BE REMOVED IN A REAL-WORLD SCENARIO 35 | options.SecurityTokenValidators.Add(TestTokenValidator()) 36 | 37 | // this is what you should look for in a real-world scenario 38 | // comment the lines if you cloned this repository and want to test the library 39 | options.Authority <- "https://login.microsoftonline.com/" 40 | options.Audience <- "" 41 | options.TokenValidationParameters <- TokenValidationParameters 42 | ( 43 | ValidateIssuer = true, 44 | ValidateAudience = true, 45 | ValidateLifetime = true, 46 | ValidateIssuerSigningKey = true 47 | ) 48 | () 49 | ) |> ignore 50 | 51 | services 52 | .AddFunctionsAuthorization(fun options -> 53 | // Add your policies here 54 | () 55 | ) |> ignore 56 | ) 57 | .Build() 58 | 59 | host.Run() 60 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/SampleIsolatedFunctionsFSharp_V4.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | v4 5 | Exe 6 | 17c2def3-36ba-461c-8cf2-2305557bb98b 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | Never 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/TestFunction.fs: -------------------------------------------------------------------------------- 1 | namespace SampleIsolatedFunctionsFSharp.V4 2 | 3 | open System.Text 4 | open DarkLoop.Azure.Functions.Authorization 5 | open Microsoft.AspNetCore.Authentication 6 | open Microsoft.AspNetCore.Authorization 7 | open Microsoft.AspNetCore.Http 8 | open Microsoft.AspNetCore.Mvc 9 | open Microsoft.Azure.Functions.Worker 10 | open Microsoft.Extensions.DependencyInjection 11 | open Microsoft.Extensions.Logging 12 | 13 | 14 | [] 15 | type TestFunction(logger:ILogger) = 16 | let _logger = logger 17 | 18 | [] 19 | [] 20 | member _.Run([] req:HttpRequest) = 21 | task { 22 | _logger.LogInformation("F# HTTP trigger function processed a request.") 23 | 24 | let provider = req.HttpContext.RequestServices 25 | let schProvider = provider.GetService() 26 | 27 | let sb = new StringBuilder() 28 | sb.AppendLine("Authentication schemes:") |> ignore 29 | 30 | if (schProvider <> null) then 31 | let! allScheme = schProvider.GetAllSchemesAsync() 32 | for scheme in allScheme do 33 | sb.AppendLine($" {scheme.Name} -> {scheme.HandlerType}") |> ignore 34 | 35 | 36 | sb.AppendLine()|> ignore 37 | sb.AppendLine($"User:")|> ignore 38 | sb.AppendLine($" Name -> {req.HttpContext.User.Identity.Name}")|> ignore 39 | let email = req.HttpContext.User.FindFirst("email")|> Option.ofObj|>Option.map _.Value 40 | sb.AppendLine($" Email -> {email}")|> ignore 41 | 42 | return OkObjectResult(sb.ToString()) 43 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /sample/SampleIsolatedFunctionsFSharp.V4/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" 6 | } 7 | } -------------------------------------------------------------------------------- /src/abstractions/Cache/FunctionsAuthorizationFilterCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Collections.Concurrent; 6 | 7 | namespace DarkLoop.Azure.Functions.Authorization.Cache 8 | { 9 | /// 10 | internal sealed class FunctionsAuthorizationFilterCache : IFunctionsAuthorizationFilterCache 11 | where TIdentifier : notnull 12 | { 13 | private readonly ConcurrentDictionary _filters = new(); 14 | 15 | /// 16 | public bool TryGetFilter(TIdentifier functionIdentifier, out FunctionAuthorizationFilter? filter) 17 | { 18 | return _filters.TryGetValue(functionIdentifier, out filter); 19 | } 20 | 21 | /// 22 | public bool SetFilter(TIdentifier functionIdentifier, FunctionAuthorizationFilter builder) 23 | { 24 | return _filters.TryAdd(functionIdentifier, builder); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/abstractions/Cache/IFunctionsAuthorizationFilterCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | namespace DarkLoop.Azure.Functions.Authorization.Cache 6 | { 7 | /// 8 | /// Provides a fully built authorization filter cache for functions. 9 | /// 10 | public interface IFunctionsAuthorizationFilterCache 11 | { 12 | /// 13 | /// Gets the authorization filter for the specified function if exists. 14 | /// 15 | bool TryGetFilter(TIdentifier functionIdentifier, out FunctionAuthorizationFilter? filter); 16 | 17 | /// 18 | /// Sets the authorization filter for the specified function. 19 | /// 20 | /// The function unique identifier 21 | /// The filter to cache. 22 | /// 23 | bool SetFilter(TIdentifier functionIdentifier, FunctionAuthorizationFilter filter); 24 | } 25 | } -------------------------------------------------------------------------------- /src/abstractions/Constants.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace DarkLoop.Azure.Functions.Authorization 8 | { 9 | [ExcludeFromCodeCoverage] 10 | internal class Constants 11 | { 12 | internal const string AuthInvokedKey = "__WebJobAuthInvoked"; 13 | internal const string WebJobsAuthScheme = "WebJobsAuthLevel"; 14 | internal const string ArmTokenAuthScheme = "ArmToken"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DarkLoop.Azure.Functions.Authorization.Abstractions 5 | DarkLoop.Azure.Functions.Authorization 6 | net6.0;net8.0 7 | 0.0.1-preview 8 | DarkLoop's Azure Functions authorization extension shared core functionality for InProc and Isolated modules. 9 | enable 10 | 11 | 12 | 13 | TRACE 14 | 15 | 16 | 17 | DEBUG;TRACE 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | True 49 | True 50 | Messages.resx 51 | 52 | 53 | 54 | 55 | 56 | ResXFileCodeGenerator 57 | Messages.Designer.cs 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/abstractions/EmptySchemeStrategy.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | namespace DarkLoop.Azure.Functions.Authorization 6 | { 7 | /// 8 | /// Strategy to use when schemes are not specified in function authorization definition. 9 | /// 10 | public enum EmptySchemeStrategy 11 | { 12 | /// 13 | /// Use all authentication schemes specified in the application. 14 | /// 15 | /// 16 | /// This does not apply to and within the In-Proc hosting model. 17 | /// 18 | UseAllSchemes, 19 | 20 | /// 21 | /// Use the default authentication scheme specified in the application. 22 | /// 23 | UseDefaultScheme, 24 | } 25 | } -------------------------------------------------------------------------------- /src/abstractions/FunctionAuthorizationContext.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using DarkLoop.Azure.Functions.Authorization.Internal; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Authorization.Policy; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | /// 13 | /// Represents the context associated with the current request authorization. 14 | /// 15 | /// 16 | [ExcludeFromCodeCoverage] 17 | public class FunctionAuthorizationContext 18 | where TContext : class 19 | { 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The name of the function. 24 | /// The platform dependent context associated with the current request. 25 | /// The authorization policy associated with the current request. 26 | /// The authorization result associated with the current request. 27 | public FunctionAuthorizationContext( 28 | string functionName, TContext httpContext, AuthorizationPolicy policy, PolicyAuthorizationResult result) 29 | { 30 | Check.NotNullOrWhiteSpace(functionName, nameof(functionName)); 31 | Check.NotNull(httpContext, nameof(httpContext)); 32 | Check.NotNull(policy, nameof(policy)); 33 | Check.NotNull(result, nameof(result)); 34 | 35 | FunctionName = functionName; 36 | UnderlyingContext = httpContext; 37 | Policy = policy; 38 | Result = result; 39 | } 40 | 41 | /// 42 | /// Gets the name of the function. 43 | /// 44 | public string FunctionName { get; } 45 | 46 | /// 47 | /// Gets the underlying context associated with the current request. 48 | /// 49 | public TContext UnderlyingContext { get; } 50 | 51 | /// 52 | /// Gets the authorization policy associated with the current request. 53 | /// 54 | public AuthorizationPolicy Policy { get; set; } 55 | 56 | /// 57 | /// Gets the authorization result associated with the current request. 58 | /// 59 | public PolicyAuthorizationResult Result { get; set; } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/abstractions/FunctionAuthorizationFeature.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization.Internal; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Http.Features.Authentication; 8 | using System.Security.Claims; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | // This was designed with maximum compatibility with ASP.NET core. It keeps 13 | // two separate features in sync with each other automatically. 14 | internal sealed class FunctionAuthorizationFeature : IAuthenticateResultFeature, IHttpAuthenticationFeature 15 | { 16 | private ClaimsPrincipal? _principal; 17 | private AuthenticateResult? _authenticateResult; 18 | 19 | /// 20 | /// Construct an instance of the feature with the given AuthenticateResult 21 | /// 22 | /// 23 | public FunctionAuthorizationFeature(AuthenticateResult result) 24 | { 25 | Check.NotNull(result, nameof(result)); 26 | 27 | AuthenticateResult = result; 28 | } 29 | 30 | /// 31 | public AuthenticateResult? AuthenticateResult 32 | { 33 | get => _authenticateResult; 34 | set 35 | { 36 | _authenticateResult = value; 37 | _principal = value?.Principal; 38 | } 39 | } 40 | 41 | /// 42 | public ClaimsPrincipal? User 43 | { 44 | get => _principal; 45 | set 46 | { 47 | _authenticateResult = null; 48 | _principal = value; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/abstractions/FunctionAuthorizationFilter.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace DarkLoop.Azure.Functions.Authorization 9 | { 10 | /// 11 | /// Represents the authorization filter for a function. 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | public sealed class FunctionAuthorizationFilter 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The to be used for the function. 20 | /// A value indicating whether the function allows anonymous access. 21 | public FunctionAuthorizationFilter(AuthorizationPolicy? authorizationPolicy, bool allowAnonymous = false) 22 | { 23 | Policy = authorizationPolicy; 24 | AllowAnonymous = allowAnonymous; 25 | } 26 | 27 | /// 28 | /// A value indicating whether the function allows anonymous access. 29 | /// 30 | public bool AllowAnonymous { get; } 31 | 32 | /// 33 | /// Gets or sets the to be used for the function. 34 | /// 35 | public AuthorizationPolicy? Policy { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/abstractions/FunctionAuthorizationMetadata.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using DarkLoop.Azure.Functions.Authorization.Internal; 9 | using Microsoft.AspNetCore.Authorization; 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization 12 | { 13 | /// 14 | /// Authorization metadata for a function or type. 15 | /// 16 | public sealed class FunctionAuthorizationMetadata 17 | { 18 | private readonly int _key; 19 | private readonly string? _functionName; 20 | private readonly Type? _declaringType; 21 | private readonly List _authData; 22 | 23 | /// 24 | /// Default authorization rule. 25 | /// 26 | public readonly static FunctionAuthorizationMetadata Empty = new() { AllowsAnonymousAccess = false }; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | private FunctionAuthorizationMetadata() => _authData = new List(); 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// The name of the function as specified in the [Function(Name)Attribute]. 37 | /// The type declaring the function method. 38 | internal FunctionAuthorizationMetadata(string functionName, Type declaringType) : this() 39 | { 40 | Check.NotNullOrWhiteSpace(functionName, nameof(functionName), "The name of the function must be specified."); 41 | Check.NotNull(declaringType, nameof(declaringType), "The declaring type of the function must be specified."); 42 | 43 | _key = GetId(functionName, declaringType); 44 | _functionName = functionName; 45 | _declaringType = declaringType; 46 | } 47 | 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// The type declaring the function method. 52 | internal FunctionAuthorizationMetadata(Type declaringType) : this() 53 | { 54 | Check.NotNull(declaringType, nameof(declaringType), "The declaring type of the function must be specified."); 55 | 56 | _key = GetId(null, declaringType); 57 | _declaringType = declaringType; 58 | } 59 | 60 | /// 61 | /// Gets the authorization ID for the function or type. 62 | /// 63 | public int AuthorizationId => _key; 64 | 65 | /// 66 | /// Gets the name of the function as specified in the [Function(Name)Attribute]. 67 | /// 68 | public string? FunctionName => _functionName; 69 | 70 | /// 71 | /// Gets the name of the type declaring the function method. 72 | /// 73 | /// The returned value is never . Only for 74 | public Type? DeclaringType => _declaringType; 75 | 76 | /// 77 | /// Gets a value indicating whether the function allows anonymous access. 78 | /// 79 | public bool AllowsAnonymousAccess { get; internal set; } 80 | 81 | /// 82 | /// Gets the authorization data for the function or type. 83 | /// 84 | public IReadOnlyList AuthorizationData => _authData.ToImmutableArray(); 85 | 86 | /// 87 | /// Adds authorization data to metadata. 88 | /// 89 | /// Authorize data. 90 | public FunctionAuthorizationMetadata AddAuthorizeData(IAuthorizeData authorizeData) 91 | { 92 | Check.NotNull(authorizeData, nameof(authorizeData), "The authorization data must be specified."); 93 | 94 | _authData.Add(authorizeData); 95 | 96 | return this; 97 | } 98 | 99 | /// 100 | /// Adds authorization data to metadata. 101 | /// 102 | /// Authorize data. 103 | public FunctionAuthorizationMetadata AddAuthorizeData(IEnumerable authorizeData) 104 | { 105 | Check.NotNull(authorizeData, nameof(authorizeData), "The authorization data must be specified."); 106 | Check.All(authorizeData, x => Check.NotNull(x, nameof(authorizeData), "All elements in authorization data must be non-null.")); 107 | 108 | _authData.AddRange(authorizeData); 109 | 110 | return this; 111 | } 112 | 113 | /// 114 | /// Allows anonymous access to the function.
115 | /// Once the value ise set, it cannot be changed. 116 | ///
117 | public FunctionAuthorizationMetadata AllowAnonymousAccess() 118 | { 119 | AllowsAnonymousAccess = true; 120 | 121 | return this; 122 | } 123 | 124 | /// 125 | /// Gets the metadata ID for a function. 126 | /// 127 | /// The name of the function. 128 | /// The type declaring the function. 129 | internal static int GetId(string? functionName, Type? declaringType) => 130 | (declaringType?.GetHashCode() ?? 0) ^ 131 | (functionName?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/abstractions/FunctionAuthorizationTypeMap.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace DarkLoop.Azure.Functions.Authorization 13 | { 14 | internal sealed class FunctionAuthorizationTypeMap 15 | { 16 | private readonly ConcurrentDictionary _typeMap = new(StringComparer.OrdinalIgnoreCase); 17 | 18 | /// 19 | /// Registers the type declaring a function with the specified . 20 | /// 21 | /// The name of the function. 22 | /// The type declaring the function. 23 | /// if the registration was successful; otherwise . 24 | internal bool AddFunctionType(string functionName, Type functionType) 25 | { 26 | return _typeMap.TryAdd(functionName, functionType); 27 | } 28 | 29 | /// 30 | /// Gets the type declaring a function with the specified . 31 | /// 32 | /// The name of the function. 33 | /// 34 | internal Type? this[string functionName] 35 | { 36 | get => _typeMap.GetValueOrDefault(functionName); 37 | } 38 | 39 | /// 40 | /// Returns a value indicating whether the function with the specified is registered. 41 | /// 42 | /// The name of the function. 43 | /// if function is registered; otherwise 44 | internal bool IsFunctionRegistered(string functionName) 45 | { 46 | return _typeMap.ContainsKey(functionName); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthenticationBuilder.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace DarkLoop.Azure.Functions.Authorization 9 | { 10 | /// 11 | /// An that enhances the built-in authentication behavior for Azure Functions. 12 | /// 13 | public class FunctionsAuthenticationBuilder : AuthenticationBuilder 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The current service collection instance. 19 | internal FunctionsAuthenticationBuilder(IServiceCollection services) 20 | : base(services) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthenticationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | 12 | public static class FunctionsAuthenticationBuilderExtensions 13 | { 14 | /// 15 | /// Adds the JWT "FunctionsBearer" scheme to the authentication configuration. 16 | /// 17 | /// The current authentication builder. 18 | /// An action configuring the JWT options for authentication. 19 | /// The authentication scheme to use." 20 | /// A instance of the 21 | public static FunctionsAuthenticationBuilder AddJwtFunctionsBearer( 22 | this FunctionsAuthenticationBuilder builder, string authenticationScheme, Action configureOptions) 23 | { 24 | if (authenticationScheme.Equals(JwtBearerDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase)) 25 | { 26 | throw new ArgumentException("Bearer scheme cannot be specified as it conflicts with Azure Functions built-in authentication", nameof(authenticationScheme)); 27 | } 28 | 29 | builder.AddJwtBearer(authenticationScheme, configureOptions); 30 | 31 | return builder; 32 | } 33 | 34 | /// 35 | /// Adds the JWT "FunctionsBearer" scheme to the authentication configuration. 36 | /// 37 | /// The current authentication builder. 38 | /// An action configuring the JWT options for authentication. 39 | /// A instance of the 40 | public static FunctionsAuthenticationBuilder AddJwtFunctionsBearer( 41 | this FunctionsAuthenticationBuilder builder, Action configureOptions) 42 | { 43 | builder.AddJwtBearer(JwtFunctionsBearerDefaults.AuthenticationScheme, configureOptions); 44 | 45 | return builder; 46 | } 47 | 48 | /// 49 | /// Adds the JWT "FunctionsBearer" scheme to the authentication configuration. 50 | /// 51 | /// The current authentication builder. 52 | /// A instance of the 53 | public static FunctionsAuthenticationBuilder AddJwtFunctionsBearer( 54 | this FunctionsAuthenticationBuilder builder) 55 | { 56 | return builder.AddJwtFunctionsBearer(_ => { }); 57 | } 58 | 59 | /// 60 | /// This is a no-op method to prevent conflicts with the built-in AddJwtBearer used by the functions host. 61 | /// 62 | /// The current builder. 63 | /// JWT options configuration. 64 | /// 65 | [Obsolete("This method should not be called without specifying a name, as it would conflict with the framework's built-in setup. Use AddJwtFunctionsBearer instead, or specify a name other than 'Bearer' for scheme.", true)] 66 | public static FunctionsAuthenticationBuilder AddJwtBearer( 67 | this FunctionsAuthenticationBuilder builder, Action configureOptions) 68 | { 69 | // This method should not be called without specifying a name, 70 | // as it would conflict with the framework's built-in setup. 71 | return builder; 72 | } 73 | 74 | /// 75 | /// This is a no-op method to prevent conflicts with the built-in AddJwtBearer used by the functions host. 76 | /// 77 | /// The current builder. 78 | /// 79 | [Obsolete("This method should not be called without specifying a name, as it would conflict with the framework's built-in setup. Use AddJwtFunctionsBearer instead, or specify a name other than 'Bearer' for scheme.", true)] 80 | public static FunctionsAuthenticationBuilder AddJwtBearer( 81 | this FunctionsAuthenticationBuilder builder) 82 | { 83 | // This method should not be called without specifying a name, 84 | // as it would conflict with the framework's built-in setup. 85 | return builder; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthorizationBuilder.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | namespace DarkLoop.Azure.Functions.Authorization 6 | { 7 | /// 8 | /// Placeholder class for future functionality. 9 | /// 10 | public class FunctionsAuthorizationBuilder 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization.Cache; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization 10 | { 11 | /// 12 | /// Extension methods for adding the Functions Authorization Core services to the DI container. 13 | /// 14 | internal static class FunctionsAuthorizationCoreServiceCollectionExtensions 15 | { 16 | public static IServiceCollection AddFunctionsAuthorizationCore(this IServiceCollection services) 17 | { 18 | services 19 | .TryAddSingleton(); 20 | 21 | return services 22 | .AddSingleton() 23 | .AddSingleton(typeof(IFunctionsAuthorizationFilterCache<>), typeof(FunctionsAuthorizationFilterCache<>)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthorizationOptions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace DarkLoop.Azure.Functions.Authorization 9 | { 10 | /// 11 | /// Options to manage Authorization functionality for Azure Functions. 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | // Important to keep this class POCO, any special functionality should be done with extension methods. 15 | public sealed class FunctionsAuthorizationOptions 16 | { 17 | internal readonly FunctionAuthorizationMetadataCollection AuthorizationMetadata = new(); 18 | 19 | /// 20 | /// Gets or sets a value indicating whether authorization is disabled. 21 | /// 22 | public bool AuthorizationDisabled {get; set;} 23 | 24 | /// 25 | /// Gets or sets a value indicating whether to write the HTTP status 26 | /// to the response when authorization failure occurs. 27 | /// 28 | public bool WriteHttpStatusToResponse { get; set; } 29 | 30 | /// 31 | /// Gets or sets the strategy to use when is empty. Default value is . 32 | /// 33 | public EmptySchemeStrategy EmptySchemeStrategy { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/abstractions/FunctionsAuthorizationResultHandler.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Net; 7 | using System.Threading.Tasks; 8 | using DarkLoop.Azure.Functions.Authorization.Internal; 9 | using Microsoft.AspNetCore.Authentication; 10 | using Microsoft.AspNetCore.Authorization.Policy; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.AspNetCore.Mvc.Abstractions; 14 | using Microsoft.AspNetCore.Routing; 15 | using Microsoft.Extensions.Options; 16 | 17 | namespace DarkLoop.Azure.Functions.Authorization 18 | { 19 | /// 20 | internal sealed class FunctionsAuthorizationResultHandler : IFunctionsAuthorizationResultHandler 21 | { 22 | private readonly IOptionsMonitor _options; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The framework options. 28 | public FunctionsAuthorizationResultHandler( 29 | IOptionsMonitor monitoredOptions) 30 | { 31 | _options = monitoredOptions; 32 | } 33 | 34 | private FunctionsAuthorizationOptions Options => _options.CurrentValue; 35 | 36 | /// 37 | public async Task HandleResultAsync( 38 | FunctionAuthorizationContext context, 39 | HttpContext httpContext, 40 | Func? onSuccess = null) 41 | where TContext : class 42 | { 43 | Check.NotNull(context, nameof(context)); 44 | Check.NotNull(httpContext, nameof(httpContext)); 45 | 46 | if (context.Result.Succeeded) 47 | { 48 | if (onSuccess is not null) 49 | { 50 | await onSuccess(context.UnderlyingContext); 51 | } 52 | 53 | return; 54 | } 55 | 56 | if (context.Result.Challenged) 57 | { 58 | if (context.Policy.AuthenticationSchemes.Count > 0) 59 | { 60 | foreach (var scheme in context.Policy.AuthenticationSchemes) 61 | { 62 | await httpContext.ChallengeAsync(scheme); 63 | } 64 | } 65 | else 66 | { 67 | await httpContext.ChallengeAsync(); 68 | } 69 | } 70 | else if (context.Result.Forbidden) 71 | { 72 | if (context.Policy.AuthenticationSchemes.Count > 0) 73 | { 74 | foreach (var scheme in context.Policy.AuthenticationSchemes) 75 | { 76 | await httpContext.ForbidAsync(scheme); 77 | } 78 | } 79 | else 80 | { 81 | await httpContext.ForbidAsync(); 82 | } 83 | } 84 | 85 | await HandleFailureAsync(httpContext, context.Result); 86 | } 87 | 88 | // Writing default results for forbidden and challenged 89 | private async Task HandleFailureAsync(HttpContext context, PolicyAuthorizationResult result) 90 | { 91 | if (Options.WriteHttpStatusToResponse && !context.Response.HasStarted) 92 | { 93 | var httpResult = default(IActionResult); 94 | 95 | if (result.Forbidden) 96 | { 97 | httpResult = new AuthorizationFailureResult(HttpStatusCode.Forbidden); 98 | } 99 | else if (result.Challenged) 100 | { 101 | httpResult = new AuthorizationFailureResult(HttpStatusCode.Unauthorized); 102 | } 103 | 104 | await httpResult!.ExecuteResultAsync(new ActionContext(context, context.GetRouteData(), new ActionDescriptor())); 105 | } 106 | } 107 | 108 | private class AuthorizationFailureResult : ObjectResult 109 | { 110 | public AuthorizationFailureResult(HttpStatusCode statusCode) : base(statusCode.ToString()) 111 | { 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/abstractions/IFunctionsAuthorizationProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace DarkLoop.Azure.Functions.Authorization 9 | { 10 | /// 11 | /// Provides a bridge between Authorization rules and filter cache. 12 | /// 13 | public interface IFunctionsAuthorizationProvider 14 | { 15 | /// 16 | /// Returns the authorization filter for the given function. 17 | /// 18 | /// The name of the function. 19 | /// The to be used to construct the policy. 20 | /// It's recommended to cache the value in this method before returning it, as this method is called for every function invocation. 21 | /// A non-null authorization filter for the function. 22 | Task GetAuthorizationAsync(string functionName, IAuthorizationPolicyProvider policyProvider); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/abstractions/IFunctionsAuthorizationResultHandler.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization 10 | { 11 | /// 12 | /// Handles the result of the authorization process. 13 | /// 14 | internal interface IFunctionsAuthorizationResultHandler 15 | { 16 | /// 17 | /// Handles the result of the authorization process. 18 | /// 19 | /// The function authorization context. 20 | /// The for the current request. 21 | /// The action to execute if the authorization process succeeded. 22 | /// 23 | Task HandleResultAsync( 24 | FunctionAuthorizationContext authorizationContext, HttpContext httpContext, Func? onSuccess = null) 25 | where TContext : class; 26 | } 27 | } -------------------------------------------------------------------------------- /src/abstractions/Internal/Check.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization.Internal 10 | { 11 | [ExcludeFromCodeCoverage] 12 | internal class Check 13 | { 14 | internal static void NotNull(object value, string name, string? message = null) 15 | { 16 | if (value == null) 17 | { 18 | throw new ArgumentNullException(name, message); 19 | } 20 | } 21 | 22 | internal static void NotNullOrWhiteSpace(string value, string name, string? message = null) 23 | { 24 | if (string.IsNullOrWhiteSpace(value)) 25 | { 26 | throw new ArgumentException(message, name); 27 | } 28 | } 29 | 30 | internal static void All(IEnumerable sequence, Action action) 31 | { 32 | foreach (var item in sequence) 33 | { 34 | action(item); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/abstractions/Internal/FunctionsAuthorizationOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Reflection; 8 | using DarkLoop.Azure.Functions.Authorization.Internal; 9 | using Microsoft.AspNetCore.Authorization; 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization 12 | { 13 | // This functionality can be exposed later through a builder to define the authorization rules for the functions 14 | // without the need to use the attributes making it more performant and flexible. 15 | internal static class FunctionsAuthorizationOptionsExtensions 16 | { 17 | /// 18 | /// Registers all the functions in the specified . 19 | /// 20 | /// The current options. 21 | /// The type containing the functions. 22 | /// A value indicating whether the function declaring type is already registered. 23 | /// Return a to keep configuring. 24 | internal static FunctionAuthorizationMetadata SetTypeAuthorizationInfo( 25 | this FunctionsAuthorizationOptions options, Type declaringType, out bool existing) 26 | { 27 | Check.NotNull(declaringType, nameof(declaringType)); 28 | 29 | return options.AuthorizationMetadata.Add(declaringType, out existing); 30 | } 31 | 32 | /// 33 | /// Registers the function with the specified name in 34 | /// in the type specified in . 35 | /// 36 | /// The current options. 37 | /// The name of the function. 38 | /// The type declaring the function. 39 | /// Return a to keep configuring. 40 | internal static FunctionAuthorizationMetadata SetFunctionAuthorizationInfo( 41 | this FunctionsAuthorizationOptions options, string functionName, Type declaringType) 42 | { 43 | Check.NotNullOrWhiteSpace(functionName, nameof(functionName)); 44 | Check.NotNull(declaringType, nameof(declaringType)); 45 | 46 | return options.AuthorizationMetadata.Add(functionName, declaringType); 47 | } 48 | 49 | /// 50 | internal static bool IsFunctionRegistered(this FunctionsAuthorizationOptions options, string functionName) 51 | { 52 | return options.AuthorizationMetadata.IsFunctionRegistered(functionName); 53 | } 54 | 55 | /// 56 | /// Registers the authorization metadata for the function extracted from attribute. 57 | /// 58 | /// The type of authorization attribute to lookup. 59 | /// The current options. 60 | /// The name of the function. 61 | /// The type declaring the function. 62 | /// The entry point method for the function. 63 | internal static void RegisterFunctionAuthorizationAttributesMetadata( 64 | this FunctionsAuthorizationOptions options, string functionName, Type declaringType, MethodInfo functionMethod) 65 | where TAuthAttribute : Attribute, IAuthorizeData 66 | { 67 | var typeMetadata = options 68 | .SetTypeAuthorizationInfo(declaringType, out var typeAlreadyRegistered); 69 | 70 | if (!typeAlreadyRegistered) 71 | { 72 | var classAuthAttributes = declaringType.GetCustomAttributes().ToArray(); 73 | var classAllowAnonymous = declaringType.GetCustomAttribute(); 74 | 75 | if (classAuthAttributes.Length > 0) 76 | { 77 | typeMetadata.AddAuthorizeData(classAuthAttributes); 78 | } 79 | 80 | if (classAllowAnonymous is not null) 81 | { 82 | typeMetadata.AllowAnonymousAccess(); 83 | } 84 | } 85 | 86 | var methodAuthAttributes = functionMethod.GetCustomAttributes().ToArray(); 87 | var methodAllowAnonymous = functionMethod.GetCustomAttribute(); 88 | var methodMetadata = options 89 | .SetFunctionAuthorizationInfo(functionName, declaringType); 90 | 91 | if (methodAuthAttributes.Length > 0) 92 | { 93 | methodMetadata.AddAuthorizeData(methodAuthAttributes); 94 | } 95 | 96 | if (methodAllowAnonymous is not null) 97 | { 98 | methodMetadata.AllowAnonymousAccess(); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/abstractions/Internal/FunctionsFeatureCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Http.Features; 7 | using Microsoft.AspNetCore.Http.Features.Authentication; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization.Internal 10 | { 11 | // This functionality is used internally to emulate Asp.net's treatment of AuthenticateResult 12 | internal static class FunctionsFeatureCollectionExtension 13 | { 14 | /// 15 | /// Store the given AuthenticateResult in the IFeatureCollection accessible via 16 | /// IAuthenticateResultFeature and IHttpAuthenticationFeature 17 | /// 18 | /// The feature collection to add to 19 | /// The authentication to expose in the feature collection 20 | /// The object associated with the features 21 | public static FunctionAuthorizationFeature SetAuthenticationFeatures(this IFeatureCollection features, AuthenticateResult result) 22 | { 23 | // A single object is used to handle both of these features so that they stay in sync. 24 | // This is in line with what asp core normally does. 25 | var feature = new FunctionAuthorizationFeature(result); 26 | 27 | features.Set(feature); 28 | features.Set(feature); 29 | 30 | return feature; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/abstractions/Internal/KeyedMonitor.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization.Internal 12 | { 13 | /// 14 | /// Provides a way to monitor a key and block other threads from entering the same key. 15 | /// 16 | internal static class KeyedMonitor 17 | { 18 | private static readonly ConcurrentDictionary __locks = new(); 19 | private static readonly Timer __cleanupTimer = new(_ => OnCleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); 20 | 21 | static KeyedMonitor() 22 | { 23 | AppDomain.CurrentDomain.ProcessExit += (s, e) => __cleanupTimer.Dispose(); 24 | } 25 | 26 | /// 27 | /// Enters the monitor for the specified key. 28 | /// 29 | /// 30 | /// Try to use very unique keys per block of code you want to protect. 31 | /// If the same ey might be used in different parts of the code, create a string and prefix with another identifier. 32 | /// 33 | /// The key to monitor. 34 | /// 35 | /// A value indicating the protected logic shouldn't need the lock any longer. 36 | /// The lock should not be monitored and should be cleaned-up internally. 37 | /// 38 | public static async Task EnterAsync(object key, bool unblockOnFirstExit = false) 39 | { 40 | var @lock = __locks.GetOrAdd(key, _ => new KeyedLock(unblockOnFirstExit)); 41 | 42 | if (!@lock.Terminated) 43 | { 44 | await @lock.LockAsync(); 45 | } 46 | } 47 | 48 | /// 49 | /// Exits the monitor for the specified key. 50 | /// 51 | /// The key blocked on. 52 | public static void Exit(object key) 53 | { 54 | if (__locks.TryGetValue(key, out var @lock)) 55 | { 56 | @lock.Unlock(); 57 | } 58 | } 59 | 60 | private static void OnCleanup() 61 | { 62 | var keysToDelete = __locks.Where(x => x.Value.Terminated).ToList(); 63 | 64 | foreach (var key in keysToDelete) 65 | { 66 | __locks.TryRemove(key.Key, out _); 67 | } 68 | } 69 | 70 | private class KeyedLock 71 | { 72 | private readonly SemaphoreSlim _monitor; 73 | private readonly bool _disposeOnFirstExit; 74 | private bool _terminated; 75 | 76 | public KeyedLock(bool disposeOnFirstExit) 77 | { 78 | _monitor = new SemaphoreSlim(1); 79 | _disposeOnFirstExit = disposeOnFirstExit; 80 | } 81 | 82 | public bool Terminated => _terminated; 83 | 84 | public async Task LockAsync() 85 | { 86 | await _monitor.WaitAsync(); 87 | } 88 | 89 | public void Unlock() 90 | { 91 | if (_terminated) 92 | { 93 | return; 94 | } 95 | 96 | if (_disposeOnFirstExit) 97 | { 98 | while (_monitor.CurrentCount == 0) 99 | { 100 | _monitor.Release(); 101 | Task.Delay(1).Wait(); 102 | } 103 | 104 | _terminated = true; 105 | _monitor.Dispose(); 106 | } 107 | else 108 | { 109 | if (_monitor.CurrentCount == 0) 110 | { 111 | _monitor.Release(); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/abstractions/JwtFunctionsBearerDefaults.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization 12 | { 13 | /// 14 | /// Default values used for Jwt Functions Bearer authentication. 15 | /// 16 | public class JwtFunctionsBearerDefaults 17 | { 18 | /// 19 | /// The default scheme used for Jwt Functions Bearer authentication. 20 | /// 21 | public const string AuthenticationScheme = "FunctionsBearer"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/abstractions/Properties/Messages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Messages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Messages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DarkLoop.Azure.Functions.Authorization.Properties.Messages", typeof(Messages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to No default authentication scheme was specified. Make sure to specify one when adding authentication to your services.. 65 | /// 66 | internal static string DefaultAuthSchemeNotSet { 67 | get { 68 | return ResourceManager.GetString("DefaultAuthSchemeNotSet", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to No authorization metadata found for function '{0}'.. 74 | /// 75 | internal static string NoAuthMetadataFoundForFunction { 76 | get { 77 | return ResourceManager.GetString("NoAuthMetadataFoundForFunction", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/abstractions/README.md: -------------------------------------------------------------------------------- 1 | # function-authorization-abstractions 2 | 3 | This module is a collection of abstractions to keep both authorization modules (Authorize and Authorization.Isolated) more in sync for future development. 4 | 5 | For more information on the dependent modules: 6 | - `DarkLoop.Azure.Functions.Authorization.InProcess` read the following [README](https://www.nuget.org/packages/DarkLoop.Azure.Functions.Authorization.InProcess). 7 | - `DarkLoop.Azure.Functions.Authorization.Isolated` read the following [README](https://www.nuget.org/packages/DarkLoop.Azure.Functions.Authorization.Isolated). -------------------------------------------------------------------------------- /src/abstractions/Security/AuthorizationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using DarkLoop.Azure.Functions.Authorization.Internal; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | #if NET7_0_OR_GREATER 13 | /// 14 | /// Extension methods for 15 | /// 16 | public static class AuthorizationBuilderExtensions 17 | { 18 | /// 19 | /// Disables authorization for functions instrumented for authorization. 20 | /// 21 | /// The current instance. 22 | /// A value indicating whether authorization is disabled. 23 | /// 24 | public static AuthorizationBuilder DisableAuthorization(this AuthorizationBuilder builder, bool disabled) 25 | { 26 | Check.NotNull(builder, nameof(builder)); 27 | 28 | builder.Services.Configure(options => options.AuthorizationDisabled = disabled); 29 | return builder; 30 | } 31 | } 32 | #endif 33 | } 34 | -------------------------------------------------------------------------------- /src/in-proc/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/in-proc/Bindings/FunctionsAuthorizeBindingProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Net.Http; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using DarkLoop.Azure.Functions.Authorize; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Host.Bindings; 13 | using Microsoft.Extensions.Options; 14 | 15 | namespace DarkLoop.Azure.Functions.Authorization.Bindings 16 | { 17 | internal class FunctionsAuthorizeBindingProvider : IBindingProvider 18 | { 19 | private readonly FunctionsAuthorizationOptions _options; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The options object. 25 | public FunctionsAuthorizeBindingProvider( 26 | IOptions options) 27 | { 28 | _options = options.Value; 29 | } 30 | 31 | /// 32 | public Task TryCreateAsync(BindingProviderContext context) 33 | { 34 | if (context == null) throw new ArgumentNullException(nameof(context)); 35 | 36 | var paramType = context.Parameter.ParameterType; 37 | if (paramType == typeof(HttpRequest) || paramType == typeof(HttpRequestMessage)) 38 | { 39 | this.ProcessAuthorization(context.Parameter); 40 | } 41 | 42 | return Task.FromResult(null); 43 | } 44 | 45 | private void ProcessAuthorization(ParameterInfo info) 46 | { 47 | var method = info.Member as MethodInfo ?? 48 | throw new InvalidOperationException($"Unable to bind authorization context for {info.Name}."); 49 | 50 | var declaringType = method.DeclaringType!; 51 | var nameAttr = method.GetCustomAttribute()!; 52 | 53 | _options.RegisterFunctionAuthorizationAttributesMetadata( 54 | nameAttr.Name, declaringType, method); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/in-proc/DarkLoop.Azure.Functions.Authorization.InProcess.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DarkLoop.Azure.Functions.Authorization 5 | DarkLoop.Azure.Functions.Authorization.InProcess 6 | net6.0;net8.0 7 | 0.0.1-preview 8 | 3472ff41-3859-4101-a2da-6c37d751fd31 9 | Azure Functions V3 and V4 (InProc) extension to enable authentication and authorization on a per function basis based on ASPNET Core frameworks. 10 | enable 11 | 6.35.0 12 | 7.1.2 13 | 14 | 15 | 16 | TRACE 17 | 18 | 19 | 20 | DEBUG;TRACE 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | all 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | True 54 | True 55 | Strings.resx 56 | 57 | 58 | 59 | 60 | 61 | ResXFileCodeGenerator 62 | Strings.Designer.cs 63 | 64 | 65 | 66 | 67 | 68 | PreserveNewest 69 | Never 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/in-proc/FunctionAuthorizationContextInternal.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Authorization.Policy; 8 | using Microsoft.AspNetCore.Http; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | /// 13 | /// Internal implementation of the function authorization context. 14 | /// 15 | internal sealed class FunctionAuthorizationContextInternal : FunctionAuthorizationContext 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | internal FunctionAuthorizationContextInternal( 21 | string functionName, HttpContext httpContext, AuthorizationPolicy policy, PolicyAuthorizationResult result) 22 | : base(functionName, httpContext, policy, result) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/in-proc/FunctionAuthorizationException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Net; 7 | using System.Runtime.Serialization; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization 10 | { 11 | /// 12 | /// Represents an exception that is thrown when an authorization error is encountered to short circuit function execution. 13 | /// 14 | public sealed class FunctionAuthorizationException : Exception 15 | { 16 | private readonly HttpStatusCode _statusCode; 17 | 18 | internal FunctionAuthorizationException(HttpStatusCode status) 19 | : base($"{ValidateStatus(status)} authorization error encountered. This is the only way to stop function execution. The correct status has been communicated to caller") 20 | { 21 | _statusCode = status; 22 | } 23 | 24 | /// 25 | /// Gets the status code that was returned to caller. 26 | /// 27 | public HttpStatusCode AuthorizationStatus => _statusCode; 28 | 29 | private static int ValidateStatus(HttpStatusCode status) 30 | { 31 | if (status != HttpStatusCode.Unauthorized && status != HttpStatusCode.Forbidden) 32 | { 33 | throw new ArgumentException("Only unauthorized and forbidden status are accepted for this exception."); 34 | } 35 | 36 | return (int)status; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/in-proc/FunctionAuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using DarkLoop.Azure.Functions.Authorization; 9 | using Microsoft.AspNetCore.Authorization; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Azure.WebJobs.Extensions.Http; 12 | using Microsoft.Azure.WebJobs.Host; 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | namespace DarkLoop.Azure.Functions.Authorize 16 | { 17 | /// 18 | /// Represents authorization logic that needs to be applied to a function. 19 | /// 20 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 21 | public class FunctionAuthorizeAttribute : FunctionInvocationFilterAttribute, IFunctionInvocationFilter, IAuthorizeData 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public FunctionAuthorizeAttribute() { } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The name of the policy used to authorize the function. 32 | public FunctionAuthorizeAttribute(string policy) 33 | { 34 | this.Policy = policy; 35 | } 36 | 37 | /// 38 | /// Gets or sets the name of the authorization policy to apply to function. 39 | /// 40 | public string? Policy { get; set; } 41 | 42 | /// 43 | /// Gets or sets a comma separated list of roles that are required to execute function. 44 | /// 45 | public string? Roles { get; set; } 46 | 47 | /// 48 | /// Gets or sets a comma separated list of authentication schemes that are required to apply the authorization logic. 49 | /// 50 | public string? AuthenticationSchemes { get; set; } 51 | 52 | async Task IFunctionInvocationFilter.OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken) 53 | { 54 | if (!IsProcessed(executingContext)) 55 | { 56 | var httpContext = executingContext.GetHttpContext(); 57 | if (httpContext is not null) 58 | { 59 | var services = httpContext.RequestServices; 60 | var authorizationExecutor = services.GetRequiredService(); 61 | 62 | await authorizationExecutor.ExecuteAuthorizationAsync(executingContext, httpContext); 63 | } 64 | } 65 | } 66 | 67 | private static bool IsProcessed(FunctionExecutingContext context) 68 | { 69 | const string valueKey = "__AuthZProcessed"; 70 | 71 | if (!context.Properties.TryGetValue(valueKey, out var value)) 72 | { 73 | context.Properties[valueKey] = true; 74 | return false; 75 | } 76 | 77 | return (bool)value; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/in-proc/FunctionExecutingContextExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Linq; 6 | using System.Net.Http; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Azure.WebJobs.Host; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | internal static class FunctionExecutingContextExtensions 13 | { 14 | internal static HttpContext? GetHttpContext(this FunctionExecutingContext functionContext) 15 | { 16 | var requestOrMessage = functionContext.Arguments.Values.FirstOrDefault(x => x is HttpRequest || x is HttpRequestMessage); 17 | 18 | if (requestOrMessage is HttpRequest request) 19 | { 20 | return request.HttpContext; 21 | } 22 | else if (requestOrMessage is HttpRequestMessage message) 23 | { 24 | return message.Properties[nameof(HttpContext)] as HttpContext; 25 | } 26 | else 27 | { 28 | return null; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/in-proc/FunctionsAuthExtension.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Microsoft.Azure.WebJobs.Description; 6 | using Microsoft.Azure.WebJobs.Host.Config; 7 | 8 | namespace DarkLoop.Azure.Functions.Authorization 9 | { 10 | [Extension("FunctionsAuthorize")] 11 | class FunctionsAuthExtension : IExtensionConfigProvider 12 | { 13 | public void Initialize(ExtensionConfigContext context) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/in-proc/FunctionsAuthorizationHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using DarkLoop.Azure.Functions.Authorization.Utils; 2 | 3 | namespace Microsoft.Azure.Functions.Extensions.DependencyInjection 4 | { 5 | /// 6 | /// Extension methods for . 7 | /// 8 | public static class FunctionsAuthorizationHostBuilderExtensions 9 | { 10 | /// 11 | /// Returns a value indicating whether the current environment is local development. 12 | /// 13 | /// The current builder. 14 | /// 15 | public static bool IsLocalAuthorizationContext(this IFunctionsHostBuilder builder) 16 | { 17 | return HostUtils.IsLocalDevelopment; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/in-proc/FunctionsAuthorizeStartup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | using DarkLoop.Azure.Functions.Authorization.Bindings; 7 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Host.Bindings; 10 | using Microsoft.Azure.WebJobs.Hosting; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | [assembly: FunctionsStartup(typeof(FunctionsAuthorizeStartup))] 14 | 15 | namespace DarkLoop.Azure.Functions.Authorization 16 | { 17 | class FunctionsAuthorizeStartup : FunctionsStartup, IWebJobsStartup 18 | { 19 | public override void Configure(IFunctionsHostBuilder builder) 20 | { 21 | builder.Services 22 | .AddFunctionsAuthorizationCore() 23 | .AddSingleton() 24 | .AddSingleton(); 25 | } 26 | 27 | void IWebJobsStartup.Configure(IWebJobsBuilder builder) 28 | { 29 | builder.AddExtension(); 30 | base.Configure(builder); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/in-proc/IFunctionsAuthorizationExecutor.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Azure.WebJobs.Host; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization 10 | { 11 | /// 12 | /// Executes the authorization for a given function. 13 | /// 14 | internal interface IFunctionsAuthorizationExecutor 15 | { 16 | /// 17 | /// Executes the authorization for a given function. 18 | /// 19 | /// The function authorization context. 20 | /// The request context. 21 | Task ExecuteAuthorizationAsync(FunctionExecutingContext context, HttpContext httpContext); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/in-proc/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/in-proc/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/in-proc/README.md: -------------------------------------------------------------------------------- 1 | # functions-authorize 2 | Bringing AuthorizeAttribute Behavior to Azure Functions v3 and v4 (In-Process) 3 | 4 | It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does. 5 | 6 | > **Breaking for current package consumers** 7 | > 8 | > Starting with version 4.1.0, due to security changes made on the Functions runtime, the Bearer scheme is no longer supported for your app functions. 9 | > 10 | > Use `AddJwtFunctionsBearer(Action)` instead of `AddJwtBearer(Action)` when setting up authentication. 11 | Using `AddJwtBearer` will generate a compilation error when used against `FunctionsAuthenticationBuilder`. 12 | We are introducing `JwtFunctionsBearerDefaults` to refer to the suggested new custom scheme name. 13 | > 14 | >No changes should be required if already using a custom scheme name. 15 | 16 | ## Using the package 17 | ### Installing the package 18 | `dotnet add package DarkLoop.Azure.Functions.Authorize` 19 | 20 | ### Setting up authentication 21 | The goal is to utilize the same authentication framework provided for ASP.NET Core 22 | ```csharp 23 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 24 | using MyFunctionAppNamespace; 25 | 26 | [assembly: FunctionsStartup(typeof(Startup))] 27 | namespace MyFunctionAppNamespace 28 | { 29 | class Startup : FunctionsStartup 30 | { 31 | public void Configure(IFunctionsHostBuilder builder) 32 | { 33 | builder 34 | .AddAuthentication(options => 35 | { 36 | options.DefaultAuthenticationScheme = JwtBearerDefaults.AuthenticationScheme; 37 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 38 | }) 39 | .AddOpenIdConnect(options => 40 | { 41 | options.ClientId = ""; 42 | // ... more options here 43 | }) 44 | // This is important as Bearer scheme is used by the runtime 45 | // and no longer supported by this framework. 46 | .AddJwtFunctionsBearer(options => 47 | { 48 | options.Audience = ""; 49 | // ... more options here 50 | }); 51 | 52 | builder 53 | .AddAuthorization(options => 54 | { 55 | options.AddPolicy("OnlyAdmins", policyBuilder => 56 | { 57 | // configure my policy requirements 58 | }); 59 | }); 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | > Starting with version 4.1.0, the default Bearer scheme is not supported by this framework. 66 | > You can use a custom scheme or make use of `AddJwtFunctionsBearer(Action)` as shown above. This one 67 | adds the `"FunctionsBearer"` scheme. Clients still submit token for Authorization header in the format: `Bearer `. 68 | 69 | 70 | No need to register the middleware the way we do for ASP.NET Core applications. 71 | 72 | ### Using the attribute 73 | And now lets use `FunctionAuthorizeAttribute` the same way we use `AuthorizeAttribute` in our ASP.NET Core applications. 74 | ```csharp 75 | public class Functions 76 | { 77 | [FunctionAuthorize] 78 | [FunctionName("get-record")] 79 | public async Task GetRecord( 80 | [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, 81 | ILogger log) 82 | { 83 | var user = req.HttpContext.User; 84 | var record = GetUserData(user.Identity.Name); 85 | return new OkObjectResult(record); 86 | } 87 | 88 | [FunctionAuthorize(Policy = "OnlyAdmins")] 89 | [FunctionName("get-all-records")] 90 | public async Task GetAllRecords( 91 | [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, 92 | ILogger log) 93 | { 94 | var records = GetAllData(); 95 | return new OkObjectResult(records); 96 | } 97 | } 98 | ``` -------------------------------------------------------------------------------- /src/in-proc/Security/FunctionsAuthenticationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Linq; 7 | using DarkLoop.Azure.Functions.Authorization; 8 | using Microsoft.AspNetCore.Authentication.JwtBearer; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace Microsoft.Extensions.DependencyInjection 12 | { 13 | /// 14 | /// Extension methods for 15 | /// 16 | public static class FunctionsAuthenticationBuilderExtensions 17 | { 18 | /// 19 | /// Adds the JWT Bearer scheme to the authentication configuration. JWT is added by default to Azure Functions 20 | /// and all HTTP functions are applied the Admin level after a token is validated. 21 | /// 22 | /// The current authentication builder. 23 | /// A value indicating whether remove the built-in configuration for JWT. 24 | /// Bearer scheme is still in place, but Admin level is not set for incoming requests. 25 | /// When setting this value to true (default) all existing configuration will be removed. 26 | /// A instance of the 27 | /// This method will be removed in future versions. 28 | [Obsolete("This method is obsolete. Using this method might break Azure portal experience. Use the AddJwtFunctionsBearer(Action) method instead.", true)] 29 | public static FunctionsAuthenticationBuilder AddJwtBearer( 30 | this FunctionsAuthenticationBuilder builder, bool removeBuiltInConfig = true) 31 | { 32 | return builder.AddJwtBearer(delegate { }, removeBuiltInConfig); 33 | } 34 | 35 | /// 36 | /// Adds the JWT Bearer scheme to the authentication configuration. JWT is added by default to Azure Functions 37 | /// and all HTTP functions are applied the Admin level after a token is validated. 38 | /// 39 | /// The current authentication builder. 40 | /// An action configuring the JWT options for authentication. 41 | /// When is set to , it enhances the built-in configuration for the scheme 42 | /// A value indicating whether remove the built-in configuration for JWT. 43 | /// Bearer scheme is still in place, but Admin level is not set incoming requests. 44 | /// When setting this value to (default) all existing configuration will be removed. 45 | /// A instance of the 46 | /// This method will be removed in future versions. 47 | [Obsolete("This method is obsolete. Using this method might break Azure portal experience. Use the AddJwtFunctionsBearer(Action) method instead.", true)] 48 | public static FunctionsAuthenticationBuilder AddJwtBearer( 49 | this FunctionsAuthenticationBuilder builder, Action configureOptions, bool removeBuiltInConfig = true) 50 | { 51 | if (removeBuiltInConfig) 52 | { 53 | var descriptors = builder.Services 54 | .Where(s => s.ServiceType == typeof(IConfigureOptions)) 55 | .ToList(); 56 | 57 | foreach (var descriptor in descriptors) 58 | { 59 | var instance = descriptor?.ImplementationInstance as ConfigureNamedOptions; 60 | 61 | if (instance?.Name == "Bearer") 62 | { 63 | builder.Services.Remove(descriptor!); 64 | } 65 | } 66 | } 67 | 68 | builder.Services 69 | .AddOptions(JwtBearerDefaults.AuthenticationScheme) 70 | .Configure(configureOptions); 71 | 72 | return builder; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/in-proc/Security/FunctionsAuthenticationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using DarkLoop.Azure.Functions.Authorization.Internal; 8 | using Microsoft.AspNetCore.Authentication; 9 | 10 | namespace Microsoft.Extensions.DependencyInjection 11 | { 12 | /// 13 | /// Extension methods for adding authentication services to the DI container. 14 | /// 15 | public static class FunctionsAuthenticationServiceCollectionExtensions 16 | { 17 | /// 18 | /// Adds Functions built-in authentication. 19 | /// 20 | public static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 21 | this IServiceCollection services, string? defaultScheme = null) 22 | { 23 | Check.NotNull(services, nameof(services)); 24 | 25 | return services.AddFunctionsAuthentication(defaultScheme, null); 26 | } 27 | 28 | /// 29 | /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication. 30 | /// 31 | /// The current service collection. 32 | /// The configuration logic. 33 | public static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 34 | this IServiceCollection services, Action? configure) 35 | { 36 | Check.NotNull(services, nameof(services)); 37 | 38 | return services.AddFunctionsAuthentication(null, configure); 39 | } 40 | 41 | private static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 42 | this IServiceCollection services, string? defaultScheme, Action? configure) 43 | { 44 | var authBuilder = new FunctionsAuthenticationBuilder(services); 45 | 46 | if (!string.IsNullOrWhiteSpace(defaultScheme)) 47 | { 48 | services.AddAuthentication(defaultScheme!); 49 | } 50 | else 51 | { 52 | services.AddAuthentication(); 53 | } 54 | 55 | authBuilder 56 | .AddScriptAuthLevel() 57 | .AddScriptJwtBearer(); 58 | 59 | if (configure is not null) 60 | { 61 | services.Configure(configure); 62 | } 63 | 64 | return authBuilder; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/in-proc/Security/FunctionsAuthorizationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.Azure.WebJobs.Script.WebHost; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | /// 12 | /// Extension methods for to add Functions built-in authorization. 13 | /// 14 | public static class FunctionsAuthorizationServiceCollectionExtensions 15 | { 16 | /// 17 | /// Adds Functions built-in authorization. 18 | /// 19 | /// The service collection to configure. 20 | public static IServiceCollection AddFunctionsAuthorization(this IServiceCollection services) 21 | { 22 | if (services is null) throw new ArgumentNullException(nameof(services)); 23 | 24 | return services.AddFunctionsAuthorization(delegate { }); 25 | } 26 | 27 | /// 28 | /// Adds Functions built-in authorization handlers and allows for further configuration. 29 | /// 30 | /// The service collection to configure. 31 | /// The method to configure the authorization options. 32 | public static IServiceCollection AddFunctionsAuthorization( 33 | this IServiceCollection services, Action configure) 34 | { 35 | if (services is null) throw new ArgumentNullException(nameof(services)); 36 | if (configure is null) throw new ArgumentNullException(nameof(configure)); 37 | 38 | services.AddWebJobsScriptHostAuthorization(); 39 | services.Configure(configure); 40 | 41 | return services; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/in-proc/Utils/HostUtils.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Reflection; 7 | using DarkLoop.Azure.Functions.Authorization.Properties; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization.Utils 10 | { 11 | internal class HostUtils 12 | { 13 | protected static readonly Assembly WebJobsHostAssembly; 14 | 15 | // These are a series of publicly available types that are used to interact with the Azure Functions runtime. 16 | // We use reflection to access these types to not create a hard dependency on the Azure Functions WebHost. 17 | internal static readonly Type? FunctionExecutionFeatureType; 18 | 19 | static HostUtils() 20 | { 21 | WebJobsHostAssembly = Assembly.Load(Strings.WJH_Assembly); 22 | 23 | if (WebJobsHostAssembly is null) 24 | { 25 | throw new InvalidOperationException($"{Assembly.GetExecutingAssembly()} cannot be used outside of an Azure Functions environment."); 26 | } 27 | 28 | var entryAssembly = Assembly.GetEntryAssembly(); 29 | var entryFullName = entryAssembly!.FullName; 30 | var entryName = entryFullName!.Substring(0, entryFullName.IndexOf(',')); 31 | IsLocalDevelopment = !entryName.Equals(Strings.WJH_Assembly, StringComparison.OrdinalIgnoreCase); 32 | } 33 | 34 | internal static bool IsLocalDevelopment { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/isolated/DarkLoop.Azure.Functions.Authorization.Isolated.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DarkLoop.Azure.Functions.Authorization.Isolated 5 | DarkLoop.Azure.Functions.Authorization 6 | net6.0;net8.0 7 | 0.0.1-preview 8 | Azure Functions V4 in Isolated mode extension to enable authentication and authorization on a per function basis based on ASPNET Core frameworks. 9 | enable 10 | 11 | 12 | 13 | TRACE 14 | 15 | 16 | 17 | DEBUG;TRACE 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | True 43 | IsolatedMessages.resx 44 | 45 | 46 | 47 | 48 | 49 | ResXFileCodeGenerator 50 | IsolatedMessages.Designer.cs 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/isolated/Extensions/FunctionContextExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Linq; 7 | using Microsoft.Azure.Functions.Worker; 8 | 9 | namespace DarkLoop.Azure.Functions.Authorization.Extensions 10 | { 11 | internal static class FunctionContextExtensions 12 | { 13 | private const string HttpTriggerBindingType = "httpTrigger"; 14 | 15 | /// 16 | /// Determines if a function is an HTTP trigger. 17 | /// 18 | /// The current function context. 19 | /// if the function is an HTTP function; otherwise . 20 | internal static bool IsHttpTrigger(this FunctionContext context) 21 | { 22 | return context.FunctionDefinition.InputBindings.Any(b => 23 | b.Value.Type.Equals(HttpTriggerBindingType, StringComparison.OrdinalIgnoreCase)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/isolated/Features/FunctionsAuthorizationFeature.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization.Internal; 6 | 7 | namespace DarkLoop.Azure.Functions.Authorization.Features 8 | { 9 | /// 10 | /// Marker class for the functions authorization feature. 11 | /// 12 | internal sealed class FunctionsAuthorizationFeature : IFunctionsAuthorizationFeature 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The function name. 18 | public FunctionsAuthorizationFeature(string name) 19 | { 20 | Check.NotNullOrWhiteSpace(name, nameof(name)); 21 | 22 | Name = name; 23 | } 24 | 25 | /// 26 | public string Name { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/isolated/Features/IFunctionsAuthorizationFeature.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | namespace DarkLoop.Azure.Functions.Authorization.Features 6 | { 7 | /// 8 | /// Marker interface for the functions authorization feature. 9 | /// 10 | internal interface IFunctionsAuthorizationFeature 11 | { 12 | /// 13 | /// Gets the function name. 14 | /// 15 | string Name { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/isolated/FunctionAuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Diagnostics; 7 | using System.Diagnostics.CodeAnalysis; 8 | using Microsoft.AspNetCore.Authorization; 9 | 10 | namespace DarkLoop.Azure.Functions.Authorization 11 | { 12 | /// 13 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 14 | [DebuggerDisplay("{ToString(),nq}")] 15 | [ExcludeFromCodeCoverage] 16 | public class FunctionAuthorizeAttribute : AuthorizeAttribute, IAuthorizeData 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public FunctionAuthorizeAttribute() 22 | : base() { } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The policy name that determines access to the resource 28 | public FunctionAuthorizeAttribute(string policy) 29 | : base(policy) { } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/isolated/FunctionsAuthorizationExtensionStartup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | using DarkLoop.Azure.Functions.Authorization.Metadata; 7 | using Microsoft.Azure.Functions.Worker; 8 | using Microsoft.Azure.Functions.Worker.Core; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | [assembly: WorkerExtensionStartup(typeof(FunctionsAuthorizationExtensionStartup))] 12 | 13 | namespace DarkLoop.Azure.Functions.Authorization 14 | { 15 | /// 16 | /// Functions authorization extension startup. 17 | /// 18 | public class FunctionsAuthorizationExtensionStartup : WorkerExtensionStartup 19 | { 20 | /// 21 | public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) 22 | { 23 | applicationBuilder.Services.AddFunctionsAuthorizationCore(); 24 | 25 | // This is the only middleware we add in startup as it executes prior to other built-in extensions. 26 | // Adding AuthorizationMiddleware at this point removes the ability to access to the request context. 27 | // Package consumer is in charge of adding the AuthorizationMiddleware by calling UseFunctionsAuthorization. 28 | applicationBuilder.UseMiddleware(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/isolated/FunctionsAuthorizationMiddleware.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Threading.Tasks; 7 | using DarkLoop.Azure.Functions.Authorization.Internal; 8 | using DarkLoop.Azure.Functions.Authorization.Properties; 9 | using Microsoft.AspNetCore.Authentication; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Authorization.Policy; 12 | using Microsoft.AspNetCore.Http.Extensions; 13 | using Microsoft.AspNetCore.Http.Features.Authentication; 14 | using Microsoft.Azure.Functions.Worker; 15 | using Microsoft.Azure.Functions.Worker.Middleware; 16 | using Microsoft.Extensions.Logging; 17 | using Microsoft.Extensions.Options; 18 | 19 | namespace DarkLoop.Azure.Functions.Authorization 20 | { 21 | /// 22 | internal sealed class FunctionsAuthorizationMiddleware : IFunctionsWorkerMiddleware 23 | { 24 | private readonly IFunctionsAuthorizationProvider _authorizationProvider; 25 | private readonly IFunctionsAuthorizationResultHandler _authorizationResultHandler; 26 | private readonly IAuthorizationPolicyProvider _policyProvider; 27 | private readonly IPolicyEvaluator _policyEvaluator; 28 | private readonly IOptionsMonitor _configOptions; 29 | private readonly ILogger _logger; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// Functions authorization provider to retrieve filters. 35 | /// Authorization handler. 36 | /// ASP.NET Core's authorization policy provider. 37 | /// ASP.NET Core's policy evaluator. 38 | /// Functions authorization configure options. 39 | /// A logger object for diagnostics. 40 | public FunctionsAuthorizationMiddleware( 41 | IFunctionsAuthorizationProvider authorizationProvider, 42 | IFunctionsAuthorizationResultHandler authorizationHandler, 43 | IAuthorizationPolicyProvider policyProvider, 44 | IPolicyEvaluator policyEvaluator, 45 | IOptionsMonitor configOptions, 46 | ILogger logger) 47 | { 48 | Check.NotNull(authorizationProvider, nameof(authorizationProvider)); 49 | Check.NotNull(authorizationHandler, nameof(authorizationHandler)); 50 | Check.NotNull(policyProvider, nameof(policyProvider)); 51 | Check.NotNull(policyEvaluator, nameof(policyEvaluator)); 52 | Check.NotNull(configOptions, nameof(configOptions)); 53 | Check.NotNull(logger, nameof(logger)); 54 | 55 | _authorizationProvider = authorizationProvider; 56 | _authorizationResultHandler = authorizationHandler; 57 | _policyProvider = policyProvider; 58 | _policyEvaluator = policyEvaluator; 59 | _configOptions = configOptions; 60 | _logger = logger; 61 | } 62 | 63 | /// 64 | public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next) 65 | { 66 | var httpContext = context.GetHttpContext() ?? throw new NotSupportedException(IsolatedMessages.NotSupportedIsolatedMode); 67 | 68 | if (this._configOptions.CurrentValue.AuthorizationDisabled) 69 | { 70 | var displayUrl = httpContext.Request.GetDisplayUrl(); 71 | 72 | _logger.LogWarning(IsolatedMessages.FunctionAuthIsDisabled, displayUrl); 73 | 74 | await next(context); 75 | return; 76 | } 77 | 78 | var filter = await _authorizationProvider.GetAuthorizationAsync(context.FunctionDefinition.Name, _policyProvider); 79 | 80 | if (filter.Policy is null) 81 | { 82 | await next(context); 83 | return; 84 | } 85 | 86 | var authenticateResult = await _policyEvaluator.AuthenticateAsync(filter.Policy, httpContext); 87 | 88 | var authenticateFeature = httpContext.Features.SetAuthenticationFeatures(authenticateResult); 89 | 90 | // We also make the features available in the FunctionContext 91 | context.Features.Set(authenticateFeature); 92 | context.Features.Set(authenticateFeature); 93 | 94 | if (filter.AllowAnonymous) 95 | { 96 | await next(context); 97 | return; 98 | } 99 | 100 | if (authenticateResult is not null && !authenticateResult.Succeeded) 101 | { 102 | _logger.LogDebug( 103 | IsolatedMessages.AuthenticationFailed, 104 | filter.Policy.AuthenticationSchemes.Count > 0 105 | ? " for " + string.Join(", ", filter.Policy.AuthenticationSchemes) 106 | : string.Empty); 107 | } 108 | 109 | var authorizeResult = await _policyEvaluator.AuthorizeAsync(filter.Policy, authenticateResult!, httpContext, httpContext); 110 | var authContext = new FunctionAuthorizationContext( 111 | context.FunctionDefinition.Name, context, filter.Policy, authorizeResult); 112 | 113 | await _authorizationResultHandler.HandleResultAsync(authContext, httpContext, async (ctx) => await next(ctx)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/isolated/FunctionsAuthorizationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using DarkLoop.Azure.Functions.Authorization.Internal; 8 | using Microsoft.AspNetCore.Authentication; 9 | using Microsoft.AspNetCore.Authorization; 10 | 11 | namespace Microsoft.Extensions.DependencyInjection 12 | { 13 | /// 14 | /// Extension methods for to add Functions built-in authorization. 15 | /// 16 | /// 17 | /// Adding this functionality to maintain compatibility with the original library. 18 | /// 19 | public static class FunctionsAuthorizationServiceCollectionExtensions 20 | { 21 | /// 22 | /// Adds Functions built-in authorization. 23 | /// 24 | /// The service collection to configure. 25 | public static IServiceCollection AddFunctionsAuthorization(this IServiceCollection services) 26 | { 27 | Check.NotNull(services, nameof(services)); 28 | 29 | return services.AddAuthorization(); 30 | } 31 | 32 | /// 33 | /// Adds Functions built-in authorization handlers and allows for further configuration. 34 | /// 35 | /// The service collection to configure. 36 | /// The method to configure the authorization options. 37 | public static IServiceCollection AddFunctionsAuthorization( 38 | this IServiceCollection services, Action configure) 39 | { 40 | Check.NotNull(services, nameof(services)); 41 | Check.NotNull(configure, nameof(configure)); 42 | 43 | return services.AddAuthorization(configure); 44 | } 45 | 46 | /// 47 | /// Adds Functions built-in authentication. 48 | /// 49 | /// The current service collection. 50 | /// The default authentication scheme. 51 | public static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 52 | this IServiceCollection services, string? defaultScheme = null) 53 | { 54 | Check.NotNull(services, nameof(services)); 55 | 56 | return services.AddFunctionsAuthentication(defaultScheme, null); 57 | } 58 | 59 | /// 60 | /// Configures authentication for the Azure Functions app. It will setup Functions built-in authentication. 61 | /// 62 | /// The current service collection. 63 | /// The configuration logic. 64 | public static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 65 | this IServiceCollection services, Action? configure) 66 | { 67 | Check.NotNull(services, nameof(services)); 68 | 69 | return services.AddFunctionsAuthentication(null, configure); 70 | } 71 | 72 | private static FunctionsAuthenticationBuilder AddFunctionsAuthentication( 73 | this IServiceCollection services, string? defaultScheme, Action? configure) 74 | { 75 | var builder = new FunctionsAuthenticationBuilder(services); 76 | 77 | if (!string.IsNullOrWhiteSpace(defaultScheme)) 78 | { 79 | services.AddAuthentication(defaultScheme); 80 | } 81 | else if (configure is not null) 82 | { 83 | services.AddAuthentication(configure); 84 | } 85 | else 86 | { 87 | services.AddAuthentication(); 88 | } 89 | 90 | return builder; 91 | 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/isolated/FunctionsAuthorizationWorkerAppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | using DarkLoop.Azure.Functions.Authorization.Features; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace Microsoft.Azure.Functions.Worker 10 | { 11 | /// 12 | /// Extension methods for adding the to the application pipeline. 13 | /// 14 | public static class FunctionsAuthorizationWorkerAppBuilderExtensions 15 | { 16 | /// 17 | /// Adds DarkLoop's Functions authorization middleware to the application pipeline. 18 | /// 19 | /// The current builder. 20 | public static IFunctionsWorkerApplicationBuilder UseFunctionsAuthorization(this IFunctionsWorkerApplicationBuilder builder) 21 | { 22 | return builder.UseWhen(context => 23 | context.Features.Get() is not null); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/isolated/Metadata/FunctionsAuthorizationMetadataMiddleware.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using DarkLoop.Azure.Functions.Authorization.Extensions; 11 | using DarkLoop.Azure.Functions.Authorization.Features; 12 | using DarkLoop.Azure.Functions.Authorization.Internal; 13 | using Microsoft.AspNetCore.Authorization; 14 | using Microsoft.Azure.Functions.Worker; 15 | using Microsoft.Azure.Functions.Worker.Middleware; 16 | using Microsoft.Extensions.Options; 17 | 18 | namespace DarkLoop.Azure.Functions.Authorization.Metadata 19 | { 20 | /// 21 | /// Classifies functions based on their extension type. 22 | /// 23 | internal sealed class FunctionsAuthorizationMetadataMiddleware : IFunctionsWorkerMiddleware 24 | { 25 | private readonly FunctionsAuthorizationOptions _options; 26 | private readonly ConcurrentDictionary _trackedHttp = new(); 27 | 28 | public FunctionsAuthorizationMetadataMiddleware( 29 | IOptions options) 30 | { 31 | _options = options.Value; 32 | } 33 | 34 | /// 35 | public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next) 36 | { 37 | if (!_trackedHttp.GetOrAdd(context.FunctionId, static (_, c) => c.IsHttpTrigger(), context)) 38 | { 39 | await next(context); 40 | return; 41 | } 42 | 43 | if (!_options.IsFunctionRegistered(context.FunctionDefinition.Name)) 44 | { 45 | await RegisterHttpTriggerAuthorizationAsync(context); 46 | } 47 | 48 | context.Features.Set( 49 | new FunctionsAuthorizationFeature(context.FunctionDefinition.Name)); 50 | 51 | await next(context); 52 | } 53 | 54 | private async Task RegisterHttpTriggerAuthorizationAsync(FunctionContext context) 55 | { 56 | // Middleware can be hit concurrently, we need to ensure this functionality 57 | // is thread-safe on a per function basis. 58 | // Ensuring key is interned before entering monitor since key is compared as object 59 | var monitorKey = string.Intern($"famm:{context.FunctionId}"); 60 | await KeyedMonitor.EnterAsync(monitorKey, unblockOnFirstExit: true); 61 | 62 | try 63 | { 64 | if (_options.IsFunctionRegistered(context.FunctionDefinition.Name)) 65 | { 66 | return; 67 | } 68 | 69 | var functionName = context.FunctionDefinition.Name; 70 | var declaringTypeName = context.FunctionDefinition.EntryPoint.LastIndexOf('.') switch 71 | { 72 | -1 => string.Empty, 73 | var index => context.FunctionDefinition.EntryPoint[..index] 74 | }; 75 | 76 | var methodName = context.FunctionDefinition.EntryPoint[(declaringTypeName.Length + 1)..]; 77 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 78 | var method = assemblies.Select(a => a.GetType(declaringTypeName, throwOnError: false)) 79 | .FirstOrDefault(t => t is not null)? 80 | .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) ?? 81 | throw new MethodAccessException( 82 | $"Method instance for function '{context.FunctionDefinition.Name}' " + 83 | $"cannot be found or cannot be accessed due to its protection level."); 84 | 85 | var declaringType = method.DeclaringType!; 86 | 87 | _options.RegisterFunctionAuthorizationAttributesMetadata(functionName, declaringType, method); 88 | } 89 | finally 90 | { 91 | KeyedMonitor.Exit(monitorKey); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/isolated/Properties/IsolatedMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DarkLoop.Azure.Functions.Authorization.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class IsolatedMessages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal IsolatedMessages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DarkLoop.Azure.Functions.Authorization.Properties.IsolatedMessages", typeof(IsolatedMessages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Policy authentication did not succeed{0}. . 65 | /// 66 | internal static string AuthenticationFailed { 67 | get { 68 | return ResourceManager.GetString("AuthenticationFailed", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Authorization through FunctionAuthorizeAttribute or AuthorizeAttribute is disabled at the application level. Skipping authorization for {0}.. 74 | /// 75 | internal static string FunctionAuthIsDisabled { 76 | get { 77 | return ResourceManager.GetString("FunctionAuthIsDisabled", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to DarkLoop Functions authorization for Isolated hosting is only supported with ASPNET Core integration.. 83 | /// 84 | internal static string NotSupportedIsolatedMode { 85 | get { 86 | return ResourceManager.GetString("NotSupportedIsolatedMode", resourceCulture); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/isolated/README.md: -------------------------------------------------------------------------------- 1 | # functions-authorization-isolated 2 | Bringing AuthorizeAttribute Behavior to Azure Functions v4 in Isolated mode. 3 | 4 | It hooks into .NET Core dependency injection container to enable authentication and authorization in the same way ASP.NET Core does. 5 | 6 | > **Breaking for current package consumers** 7 | > 8 | > Starting with version 4.1.0, due to security changes made on the Functions runtime, the Bearer scheme is no longer supported for your app functions. 9 | > 10 | > Use `AddJwtFunctionsBearer(Action)` instead of `AddJwtBearer(Action)` when setting up authentication. 11 | Using `AddJwtBearer` will generate a compilation error when used against `FunctionsAuthenticationBuilder`. 12 | We are introducing `JwtFunctionsBearerDefaults` to refer to the suggested new custom scheme name. 13 | > 14 | >No changes should be required if already using a custom scheme name. 15 | 16 | ## Using the package 17 | ### Installing the package 18 | `dotnet add package DarkLoop.Azure.Functions.Authorization.Isolated` 19 | 20 | ### Setting up authentication and authorization 21 | The goal is to utilize the same authentication framework provided for ASP.NET Core 22 | ```csharp 23 | using Microsoft.AspNetCore.Authentication.JwtBearer; 24 | using Microsoft.Azure.Functions.Worker; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.Extensions.Hosting; 27 | 28 | var host = new HostBuilder() 29 | .ConfigureFunctionsWebAppliction(builder => 30 | { 31 | // Explicitly adding the extension middleware because 32 | // registering middleware when extension is loaded does not 33 | // place the middleware in the pipeline where required request 34 | // information is available. 35 | builder.UseFunctionsAuthorization(); 36 | }) 37 | .ConfigureServices(services => 38 | { 39 | services 40 | .AddFunctionsAuthentication(JwtBearerDefaults.AuthenticationScheme) 41 | // This is important as Bearer scheme is used by the runtime 42 | // and no longer supported by this framework. 43 | .AddJwtFunctionsBearer(options => 44 | { 45 | options.Authority = "https://login.microsoftonline.com/your-tenant-id"; 46 | options.Audience = "your-app-id-uri"; 47 | ... 48 | }); 49 | 50 | services.AddFunctionsAuthorization(options => 51 | { 52 | options.AddPolicy("OnlyAdmins", policy => policy.RequireRole("Admin")); 53 | }); 54 | 55 | // Add other services 56 | }) 57 | .Build(); 58 | 59 | host.Run(); 60 | ``` 61 | 62 | > Starting with version 4.1.0, the default Bearer scheme is not supported by this framework. 63 | > You can use a custom scheme or make use of `AddJwtFunctionsBearer(Action)` as shown above. This one 64 | adds the `"FunctionsBearer"` scheme. Clients still submit token for Authorization header in the format: `Bearer `. 65 | 66 | 67 | Notice the call to `UseFunctionsAuthorization` in the `ConfigureFunctionsWebAppliction` method. 68 | This is required to ensure that the middleware is placed in the pipeline where required function information is available.` 69 | 70 | Mind that the startup if coding in F# will be somewhat different. Please do check the [sample for F#](../../sample/SampleIsolatedFunctionsFSharp.V4/Program.fs) 71 | 72 | ### Using the attribute 73 | And now lets use `FunctionAuthorizeAttribute` the same way we use `AuthorizeAttribute` in our ASP.NET Core applications. 74 | ```csharp 75 | [FunctionAuthorize] 76 | public class Functions 77 | { 78 | [FunctionName("get-record")] 79 | public async Task GetRecord( 80 | [HttpTrigger("get")] HttpRequest req, ILogger log) 81 | { 82 | var user = req.HttpContext.User; 83 | var record = GetUserData(user.Identity.Name); 84 | return new OkObjectResult(record); 85 | } 86 | 87 | [Authorize(Policy = "OnlyAdmins")] 88 | [FunctionName("get-all-records")] 89 | public async Task GetAllRecords( 90 | [HttpTrigger("get")] HttpRequest req, ILogger log) 91 | { 92 | var records = GetAllData(); 93 | return new OkObjectResult(records); 94 | } 95 | } 96 | ``` 97 | 98 | Something really nice to notice is that for Functions in Isolated mode, the `HttpTriggerAttribute` default `AuthenticationLevel` is `Anonymous`, playing really well with the attribute.
99 | 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. 100 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/Abstractions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/Fakes/AuthorizeDataFake.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace Abstractions.Tests.Fakes 9 | { 10 | [ExcludeFromCodeCoverage] 11 | internal class AuthorizeDataFake : IAuthorizeData 12 | { 13 | public string? Policy { get; set; } 14 | public string? Roles { get; set; } 15 | public string? AuthenticationSchemes { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/FunctionAuthorizationMetadataCollectionTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Abstractions.Tests.Fakes; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | 8 | namespace Abstractions.Tests 9 | { 10 | [TestClass] 11 | public class FunctionAuthorizationMetadataCollectionTests 12 | { 13 | [TestMethod("MetadataCollection: should return same instance for same type")] 14 | public void MetadataCollectionShouldReturnSameInstanceForSameType() 15 | { 16 | // Arrange 17 | var collection = new FunctionAuthorizationMetadataCollection(); 18 | 19 | // Act 20 | var metadata1 = collection.Add(this.GetType(), out var existsFirst); 21 | var metadata2 = collection.Add(this.GetType(), out var existsSecond); 22 | 23 | // Assert 24 | Assert.AreSame(metadata1, metadata2); 25 | Assert.IsFalse(existsFirst); 26 | Assert.IsTrue(existsSecond); 27 | } 28 | 29 | [TestMethod("MetadataCollection: should return different instance for different type")] 30 | public void MetadataCollectionShouldReturnDifferentInstanceForDifferentType() 31 | { 32 | // Arrange 33 | var collection = new FunctionAuthorizationMetadataCollection(); 34 | 35 | // Act 36 | var metadata1 = collection.Add(this.GetType(), out var existsFirst); 37 | var metadata2 = collection.Add(typeof(FunctionAuthorizationMetadataCollection), out var existsSecond); 38 | 39 | // Assert 40 | Assert.AreNotSame(metadata1, metadata2); 41 | Assert.IsFalse(existsFirst); 42 | Assert.IsFalse(existsSecond); 43 | } 44 | 45 | [TestMethod("MetadataCollection: should return same instance for same function and type")] 46 | public void MetadataCollectionShouldReturnSameInstanceForSameFunctionAndType() 47 | { 48 | // Arrange 49 | var collection = new FunctionAuthorizationMetadataCollection(); 50 | 51 | // Act 52 | var metadata1 = collection.Add("TestFunction", this.GetType()); 53 | var metadata2 = collection.Add("TestFunction", this.GetType()); 54 | 55 | // Assert 56 | Assert.AreSame(metadata1, metadata2); 57 | Assert.AreEqual(metadata1.AuthorizationId, metadata2.AuthorizationId); 58 | } 59 | 60 | [TestMethod("MetadataCollection: should return different instance for different function and type")] 61 | public void MetadataCollectionShouldReturnDifferentInstanceForDifferentFunctionAndType() 62 | { 63 | // Arrange 64 | var collection = new FunctionAuthorizationMetadataCollection(); 65 | 66 | // Act 67 | var metadata1 = collection.Add("TestFunction", this.GetType()); 68 | var metadata2 = collection.Add("TestFunction", typeof(FunctionAuthorizationMetadataCollection)); 69 | 70 | // Assert 71 | Assert.AreNotSame(metadata1, metadata2); 72 | Assert.AreNotEqual(metadata1.AuthorizationId, metadata2.AuthorizationId); 73 | } 74 | 75 | [TestMethod("MetadataCollection: should aggregate type and function metadata")] 76 | public void MetadataCollectionShouldAggregateTypeAndFunctionMetadata() 77 | { 78 | // Arrange 79 | var collection = new FunctionAuthorizationMetadataCollection(); 80 | 81 | // Act 82 | var metadata1 = collection 83 | .Add(this.GetType(), out _) 84 | .AddAuthorizeData(new AuthorizeDataFake{ Policy = "Policy1" }); 85 | 86 | var metadata2 = collection 87 | .Add("TestFunction", this.GetType()) 88 | .AddAuthorizeData(new AuthorizeDataFake{ Policy = "Policy2"}) 89 | .AllowAnonymousAccess(); 90 | 91 | var single = collection 92 | .GetMetadata("TestFunction"); 93 | 94 | // Assert 95 | Assert.AreNotSame(metadata1, metadata2); 96 | Assert.AreNotSame(metadata2, single); 97 | Assert.AreEqual(metadata2.AuthorizationId, single.AuthorizationId); 98 | Assert.AreEqual(2, single.AuthorizationData.Count); 99 | 100 | Assert.AreSame("Policy1", single.AuthorizationData[0].Policy); 101 | Assert.AreSame("Policy2", single.AuthorizationData[1].Policy); 102 | Assert.IsTrue(single.AllowsAnonymousAccess); 103 | } 104 | 105 | [TestMethod("MetadataCollection: AllowAnonymousAccess on type should inherit to function")] 106 | public void MetadataCollectionAllowAnonymousAccessOnTypeShouldInheritToFunction() 107 | { 108 | // Arrange 109 | var collection = new FunctionAuthorizationMetadataCollection(); 110 | 111 | // Act 112 | var metadata1 = collection 113 | .Add(this.GetType(), out _) 114 | .AllowAnonymousAccess(); 115 | 116 | var metadata2 = collection 117 | .Add("TestFunction", this.GetType()); 118 | 119 | var metadata = collection 120 | .GetMetadata("TestFunction"); 121 | 122 | // Assert 123 | Assert.AreNotSame(metadata1, metadata); 124 | Assert.IsTrue(metadata.AllowsAnonymousAccess); 125 | } 126 | 127 | [TestMethod("MetadataCollection: should return metadata for unregistered function")] 128 | public void MetadataCollectionShouldReturnEmptyMetadataForUnregisteredFunction() 129 | { 130 | // Arrange 131 | var collection = new FunctionAuthorizationMetadataCollection(); 132 | 133 | // Act 134 | var metadata = collection.GetMetadata("TestFunction"); 135 | 136 | // Assert 137 | Assert.IsNotNull(metadata); 138 | Assert.AreEqual(0, metadata.AuthorizationData.Count); 139 | Assert.IsFalse(metadata.AllowsAnonymousAccess); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/FunctionAuthorizationMetadataTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Abstractions.Tests.Fakes; 12 | using DarkLoop.Azure.Functions.Authorization; 13 | using Microsoft.AspNetCore.Authorization; 14 | 15 | namespace Abstractions.Tests 16 | { 17 | [TestClass] 18 | public class FunctionAuthorizationMetadataTests 19 | { 20 | [TestMethod("Metadata: should return the same number of authorization data elements it received")] 21 | public void Metadata_ShouldReturnTheSameNumberOfAuthorizationDataElementsItReceived() 22 | { 23 | // Arrange 24 | var metadata = new FunctionAuthorizationMetadata("TestFunction", this.GetType()); 25 | 26 | // Act 27 | metadata 28 | .AddAuthorizeData(new AuthorizeDataFake()) 29 | .AddAuthorizeData(new[] { new AuthorizeDataFake(), new AuthorizeDataFake() }); 30 | 31 | 32 | // Assert 33 | Assert.AreEqual(3, metadata.AuthorizationData.Count); 34 | } 35 | 36 | [TestMethod("Metadata: GetId for Empty values and GetId with null params should get same result")] 37 | public void Metadata_GetIdForEmptyValuesAndGetIdWithNullParamsShouldGetSameResult() 38 | { 39 | // Arrange 40 | var empty = FunctionAuthorizationMetadata.Empty; 41 | 42 | // Act 43 | var result1 = FunctionAuthorizationMetadata.GetId(null, null); 44 | var result2 = FunctionAuthorizationMetadata.GetId(empty.FunctionName, empty.DeclaringType); 45 | 46 | // Assert 47 | Assert.AreEqual(result1, result2); 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/FunctionAuthorizationTypeMapTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | 7 | namespace Abstractions.Tests 8 | { 9 | [TestClass] 10 | public class FunctionAuthorizationTypeMapTests 11 | { 12 | [TestMethod("FunctionAuthorizationTypeMap: should return same instance for same type")] 13 | public void FunctionAuthorizationTypeMapShouldReturnSameInstanceForSameType() 14 | { 15 | // Arrange 16 | var map = new FunctionAuthorizationTypeMap(); 17 | var functionName = "TestFunction"; 18 | 19 | // Act 20 | var result1 = map.AddFunctionType(functionName, this.GetType()); 21 | var result2 = map.AddFunctionType(functionName, this.GetType()); 22 | 23 | // Assert 24 | Assert.IsTrue(result1); 25 | Assert.IsFalse(result2); 26 | } 27 | 28 | [TestMethod("FunctionAuthorizationTypeMap: indexer should return type for existing map")] 29 | public void FunctionAuthorizationTypeMapIndexerShouldReturnTypeForExistingMap() 30 | { 31 | // Arrange 32 | var map = new FunctionAuthorizationTypeMap(); 33 | var functionName = "TestFunction"; 34 | map.AddFunctionType(functionName, this.GetType()); 35 | 36 | // Act 37 | var result = map[functionName]; 38 | 39 | // Assert 40 | Assert.AreEqual(this.GetType(), result); 41 | } 42 | 43 | [TestMethod("FunctionAuthorizationTypeMap: indexer should return null for non-existing map")] 44 | public void FunctionAuthorizationTypeMapIndexerShouldReturnNullForNonExistingMap() 45 | { 46 | // Arrange 47 | var map = new FunctionAuthorizationTypeMap(); 48 | var functionName = "TestFunction"; 49 | 50 | // Act 51 | var result = map[functionName]; 52 | 53 | // Assert 54 | Assert.IsNull(result); 55 | } 56 | 57 | [TestMethod("FunctionAuthorizationTypeMap: IsFunctionRegistered should return true for existing map")] 58 | public void FunctionAuthorizationTypeMapIsFunctionRegisteredShouldReturnTrueForExistingMap() 59 | { 60 | // Arrange 61 | var map = new FunctionAuthorizationTypeMap(); 62 | var functionName = "TestFunction"; 63 | map.AddFunctionType(functionName, this.GetType()); 64 | 65 | // Act 66 | var result = map.IsFunctionRegistered(functionName); 67 | 68 | // Assert 69 | Assert.IsTrue(result); 70 | } 71 | 72 | [TestMethod("FunctionAuthorizationTypeMap: IsFunctionRegistered should return false for non-existing map")] 73 | public void FunctionAuthorizationTypeMapIsFunctionRegisteredShouldReturnFalseForNonExistingMap() 74 | { 75 | // Arrange 76 | var map = new FunctionAuthorizationTypeMap(); 77 | var functionName = "TestFunction"; 78 | 79 | // Act 80 | var result = map.IsFunctionRegistered(functionName); 81 | 82 | // Assert 83 | Assert.IsFalse(result); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/FunctionsAuthorizationFilterCacheTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | using DarkLoop.Azure.Functions.Authorization.Cache; 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Authorization.Infrastructure; 10 | 11 | namespace Abstractions.Tests 12 | { 13 | [TestClass] 14 | public class FunctionsAuthorizationFilterCacheTests 15 | { 16 | [TestMethod("FilterCache: SetFilter should not replace existing instance")] 17 | public void SetFilterShouldNotReplaceExisting() 18 | { 19 | // Arrange 20 | var cache = new FunctionsAuthorizationFilterCache(); 21 | var filter = new FunctionAuthorizationFilter(null, true); 22 | 23 | // Act 24 | cache.SetFilter(1, filter); 25 | cache.SetFilter(1, new FunctionAuthorizationFilter( 26 | new AuthorizationPolicy( 27 | new[] { new DenyAnonymousAuthorizationRequirement() }, 28 | new[] { JwtBearerDefaults.AuthenticationScheme }))); 29 | 30 | // Assert 31 | cache.TryGetFilter(1, out var extractedFilter); 32 | Assert.AreSame(filter, extractedFilter); 33 | Assert.AreSame(null, filter.Policy); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/Abstractions.Tests/Internal/KeyedMonitorTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization.Internal; 6 | 7 | namespace Abstractions.Tests.Internal 8 | { 9 | [TestClass] 10 | public class KeyedMonitorTests 11 | { 12 | private bool _flag; 13 | private List? _executedTasks; 14 | private List? _unmonitored; 15 | private List? _monitored; 16 | private List? _winner; 17 | 18 | [TestInitialize] 19 | public void Initialize() 20 | { 21 | _flag = false; 22 | _executedTasks = new List(); 23 | _unmonitored = new List(); 24 | _monitored = new List(); 25 | _winner = new List(); 26 | } 27 | 28 | [TestMethod("KeyedMonitor: should allow for other threads to unblock after first exit")] 29 | public async Task KeyedMonitorShouldAllowSilentExitAfterFirstExit() 30 | { 31 | // Arrange 32 | var task1 = () => MonitoredLogicAsync("task1", 200); 33 | var task2 = () => MonitoredLogicAsync("task2", 0); 34 | var task3 = () => MonitoredLogicAsync("task3", 0); 35 | var task4 = () => MonitoredLogicAsync("task4", 20); 36 | var task5 = () => MonitoredLogicAsync("task5", 0); 37 | 38 | // Act 39 | _ = Task.Run(() => task1()); 40 | 41 | // waiting 100ms to ensure task1 enters first; 42 | await Task.Delay(50); 43 | 44 | _ = Task.Run(() => task2()); 45 | _ = Task.Run(() => task3()); 46 | _ = Task.Run(() => task4()); 47 | 48 | await Task.Delay(180); 49 | 50 | _ = Task.Run(() => task5()); 51 | 52 | // awaiting for all tasks to complete 53 | await Task.Delay(500); 54 | 55 | // Assert 56 | Assert.AreEqual(5, _executedTasks!.Count); 57 | Assert.AreEqual(1, _unmonitored!.Count); 58 | Assert.AreEqual(4, _monitored!.Count); 59 | Assert.AreEqual(1, _winner!.Count); 60 | Assert.IsTrue(_flag); 61 | } 62 | 63 | private async Task MonitoredLogicAsync(string name, int millisecondsToBlock) 64 | { 65 | _executedTasks!.Add(name); 66 | 67 | if (_flag) 68 | { 69 | _unmonitored!.Add(name); 70 | return; 71 | } 72 | 73 | await KeyedMonitor.EnterAsync("x", unblockOnFirstExit: true); 74 | 75 | try 76 | { 77 | await Task.Delay(millisecondsToBlock); 78 | _monitored!.Add(name); 79 | 80 | if (_flag) 81 | { 82 | return; 83 | } 84 | 85 | _flag = true; 86 | _winner!.Add(name); 87 | } 88 | finally 89 | { 90 | KeyedMonitor.Exit("x"); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/Abstractions.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /test/Common.Tests/Common.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/Common.Tests/HttpUtils.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.IO.Pipelines; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Http.Features; 11 | using Microsoft.AspNetCore.Routing; 12 | using Moq; 13 | 14 | namespace Common.Tests 15 | { 16 | public static class HttpUtils 17 | { 18 | public static HttpContext SetupHttpContext(IServiceProvider services) 19 | { 20 | var httpContextMock = new Mock(); 21 | var responseMock = new Mock(); 22 | var responseStream = new MemoryStream(); 23 | var requestMock = new Mock(); 24 | var requestStream = new MemoryStream(); 25 | var streamReader = PipeReader.Create(requestStream); 26 | var requestHeaders = new HeaderDictionary(); 27 | var streamWriter = PipeWriter.Create(responseStream); 28 | var features = new FeatureCollection(); 29 | 30 | httpContextMock.SetupGet(x => x.RequestServices).Returns(services); 31 | httpContextMock.SetupGet(x => x.Request).Returns(requestMock.Object); 32 | httpContextMock.SetupGet(x => x.Response).Returns(responseMock.Object); 33 | httpContextMock.SetupGet(x => x.Features).Returns(features); 34 | httpContextMock.SetupGet(x => x.Items).Returns(new Dictionary()); 35 | requestMock.SetupGet(x => x.RouteValues).Returns(new RouteValueDictionary()); 36 | requestMock.SetupGet(x => x.Body).Returns(requestStream); 37 | requestMock.SetupGet(x => x.Headers).Returns(requestHeaders); 38 | requestMock.SetupGet(x => x.HttpContext).Returns(httpContextMock.Object); 39 | responseMock.SetupGet(x => x.HasStarted).Returns(false); 40 | responseMock.SetupGet(x => x.Body).Returns(responseStream); 41 | responseMock.SetupGet(x => x.BodyWriter).Returns(streamWriter); 42 | responseMock.SetupGet(x => x.Headers).Returns(new HeaderDictionary()); 43 | responseMock.SetupGet(x => x.HttpContext).Returns(httpContextMock.Object); 44 | 45 | return httpContextMock.Object; 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Common.Tests/JwtUtils.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Linq; 9 | using System.Security.Claims; 10 | using System.Security.Cryptography; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using Microsoft.IdentityModel.Tokens; 14 | 15 | namespace Common.Tests 16 | { 17 | public static class JwtUtils 18 | { 19 | private const string __jwtTestSecret = "f24847d0e55027928dd04b707a8d26ce24ed0dd0d7a0c19f545cc0c82c6ac7ae"; 20 | 21 | public static string GenerateJwtToken(IEnumerable? claims) 22 | { 23 | var tokenHandler = new JwtSecurityTokenHandler(); 24 | var key = Encoding.Unicode.GetBytes(__jwtTestSecret); 25 | var tokenDescriptor = new SecurityTokenDescriptor 26 | { 27 | Subject = new ClaimsIdentity(claims), 28 | Expires = DateTime.UtcNow.AddDays(1), 29 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) 30 | }; 31 | 32 | var token = tokenHandler.CreateToken(tokenDescriptor); 33 | return tokenHandler.WriteToken(token); 34 | } 35 | 36 | public static SymmetricSecurityKey GetSigningKey() 37 | { 38 | var key = Encoding.Unicode.GetBytes(__jwtTestSecret); 39 | return new SymmetricSecurityKey(key); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Common.Tests/LoggerUtils.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System; 6 | using Microsoft.Extensions.Logging; 7 | using Moq; 8 | 9 | namespace Common.Tests 10 | { 11 | public static class LoggerUtils 12 | { 13 | public static ILogger CreateLogger(LogLevel level = LogLevel.None, Action? onLog = null) 14 | { 15 | var mock = new Mock>(); 16 | 17 | mock 18 | .Setup(l => l.Log( 19 | It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) 20 | .Callback((lvl, id, state, exception, formatter) => 21 | { 22 | if (lvl < level) return; 23 | 24 | Console.WriteLine($"[{lvl}]: {formatter.DynamicInvoke(state, exception)}"); 25 | onLog?.Invoke(lvl, id, state, exception, formatter); 26 | }); 27 | 28 | return mock.Object; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Common.Tests/TestTokenValidator.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | using Microsoft.IdentityModel.Tokens; 8 | 9 | namespace Common.Tests 10 | { 11 | public class TestTokenValidator : ISecurityTokenValidator 12 | { 13 | public bool CanValidateToken => true; 14 | 15 | public int MaximumTokenSizeInBytes { get; set; } 16 | 17 | public bool CanReadToken(string securityToken) 18 | { 19 | return true; 20 | } 21 | 22 | public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) 23 | { 24 | var token = new JwtSecurityToken(securityToken); 25 | validatedToken = token; 26 | 27 | return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "Local", "name", "role")); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/InProc.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /test/InProc.Tests/InProc.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/Isolated.Tests/ConcurrentTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Net; 6 | 7 | namespace Isolated.Tests 8 | { 9 | [TestClass] 10 | public class ConcurrentTests 11 | { 12 | [TestMethod] 13 | [Ignore("This is to test middleware concurrency")] 14 | public async Task TestFunctionAuthorizationMetadataCollectionAsync() 15 | { 16 | // Arrange 17 | var client = new HttpClient { BaseAddress = new Uri("http://localhost:7005/") }; 18 | 19 | // Act 20 | var message1 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); 21 | var message2 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); 22 | var message3 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); 23 | var message4 = new HttpRequestMessage(HttpMethod.Get, "api/testfunction"); 24 | var request1 = client.SendAsync(message1); 25 | var request2 = client.SendAsync(message2); 26 | var request3 = client.SendAsync(message3); 27 | var request4 = client.SendAsync(message4); 28 | 29 | // Assert 30 | await Task.WhenAll(request1, request2, request3, request4); 31 | Assert.AreEqual(HttpStatusCode.Unauthorized, request1.Result.StatusCode); 32 | Assert.AreEqual(HttpStatusCode.Unauthorized, request2.Result.StatusCode); 33 | Assert.AreEqual(HttpStatusCode.Unauthorized, request3.Result.StatusCode); 34 | Assert.AreEqual(HttpStatusCode.Unauthorized, request4.Result.StatusCode); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Isolated.Tests/Fakes/FakeFunctionClass.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Azure.Functions.Worker; 10 | 11 | namespace Isolated.Tests.Fakes 12 | { 13 | [AllowAnonymous] 14 | public class FakeFunctionClass 15 | { 16 | [Function("TestFunction")] 17 | [FunctionAuthorize] 18 | public IActionResult TestFunction([HttpTrigger("get")] HttpRequest request) 19 | { 20 | return new OkResult(); 21 | } 22 | 23 | [Function("TestFunction2")] 24 | [FunctionAuthorize] 25 | internal IActionResult TestFunction2([HttpTrigger("get")] HttpRequest request) 26 | { 27 | return new OkResult(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/Isolated.Tests/Fakes/FakeInvocationFeatures.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using Microsoft.Azure.Functions.Worker; 6 | using System.Collections; 7 | 8 | namespace Isolated.Tests.Fakes 9 | { 10 | internal sealed class FakeInvocationFeatures : IInvocationFeatures 11 | { 12 | private Dictionary _underlyingSet = new Dictionary(); 13 | 14 | public T? Get() 15 | { 16 | _underlyingSet.TryGetValue(typeof(T), out var feature); 17 | 18 | return (T?)feature; 19 | } 20 | 21 | public IEnumerator> GetEnumerator() 22 | { 23 | return _underlyingSet.GetEnumerator(); 24 | } 25 | 26 | public void Set(T instance) 27 | { 28 | _underlyingSet.Add(typeof(T), instance); 29 | } 30 | 31 | IEnumerator IEnumerable.GetEnumerator() 32 | { 33 | return _underlyingSet.GetEnumerator(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Isolated.Tests/Features/FunctionsAuthorizationFeatureTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using DarkLoop.Azure.Functions.Authorization.Features; 6 | 7 | namespace Isolated.Tests.Features 8 | { 9 | [TestClass] 10 | public class FunctionsAuthorizationFeatureTests 11 | { 12 | [TestMethod("Feature: should return name instance as passed in the constructor")] 13 | public void FeatureShouldReturnNameInstanceAsPassedInTheConstructor() 14 | { 15 | // Arrange 16 | var name = "TestFunction"; 17 | 18 | // Act 19 | var feature = new FunctionsAuthorizationFeature(name); 20 | 21 | // Assert 22 | Assert.AreEqual(name, feature.Name); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Isolated.Tests/FunctionContextExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Collections.Immutable; 6 | using DarkLoop.Azure.Functions.Authorization.Extensions; 7 | using Microsoft.Azure.Functions.Worker; 8 | using Moq; 9 | 10 | namespace Isolated.Tests 11 | { 12 | [TestClass] 13 | public class FunctionContextExtensionsTests 14 | { 15 | [TestMethod("FunctionContextExtensions: should return true when httpTrigger binding")] 16 | public void FunctionContextExtensionsShouldReturnTrueWhenHttpTriggerBinding() 17 | { 18 | // Arrange 19 | var binding = new Mock(); 20 | binding.SetupGet(binding => binding.Type).Returns("httpTrigger"); 21 | 22 | var bindingsBuilder = ImmutableDictionary.CreateBuilder(); 23 | bindingsBuilder.Add("request", binding.Object); 24 | 25 | var contextMock = new Mock(); 26 | contextMock 27 | .Setup(contextMock => contextMock.FunctionDefinition.InputBindings) 28 | .Returns(bindingsBuilder.ToImmutable()); 29 | 30 | // Act 31 | var result = contextMock.Object.IsHttpTrigger(); 32 | 33 | // Assert 34 | Assert.IsTrue(result); 35 | } 36 | 37 | [TestMethod("FunctionContextExtensions: should return false when no httpTrigger binding")] 38 | public void FunctionContextExtensionsShouldReturnFalseWhenNoHttpTriggerBinding() 39 | { 40 | // Arrange 41 | var binding = new Mock(); 42 | binding.SetupGet(binding => binding.Type).Returns("timerTrigger"); 43 | 44 | var bindingsBuilder = ImmutableDictionary.CreateBuilder(); 45 | bindingsBuilder.Add("cron", binding.Object); 46 | 47 | var contextMock = new Mock(); 48 | contextMock 49 | .Setup(contextMock => contextMock.FunctionDefinition.InputBindings) 50 | .Returns(bindingsBuilder.ToImmutable()); 51 | 52 | // Act 53 | var result = contextMock.Object.IsHttpTrigger(); 54 | 55 | // Assert 56 | Assert.IsFalse(result); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /test/Isolated.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /test/Isolated.Tests/Isolated.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/Isolated.Tests/Metadata/FunctionsAuthorizationMetadataMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) DarkLoop. All rights reserved. 3 | // 4 | 5 | using System.Collections.Immutable; 6 | using DarkLoop.Azure.Functions.Authorization; 7 | using DarkLoop.Azure.Functions.Authorization.Metadata; 8 | using Isolated.Tests.Fakes; 9 | using Microsoft.Azure.Functions.Worker; 10 | using Microsoft.Extensions.Options; 11 | using Moq; 12 | 13 | namespace Isolated.Tests.Metadata 14 | { 15 | [TestClass] 16 | public partial class FunctionsAuthorizationMetadataMiddlewareTests 17 | { 18 | private IOptions? _options; 19 | 20 | [TestInitialize] 21 | public void Setup() 22 | { 23 | _options = Options.Create(new FunctionsAuthorizationOptions()); 24 | } 25 | 26 | [TestMethod("MetadataMiddleware: should skip over non http trigger")] 27 | public async Task MetadataMiddlewareShouldSkipOverNonHttpTrigger() 28 | { 29 | // Arrange 30 | var functionId = "098039840"; 31 | var metadata = new FunctionsAuthorizationMetadataMiddleware(_options!); 32 | var functionContext = SetupFunctionContext(functionId, "TestFunction", "TestFunction", "timerTrigger", "cron"); 33 | 34 | // Act 35 | await metadata.Invoke(functionContext, async fc => await Task.CompletedTask); 36 | 37 | // Assert 38 | Assert.AreEqual(0, _options!.Value.AuthorizationMetadata.Count); 39 | } 40 | 41 | [TestMethod("MetadataMiddleware: should register function when not registered")] 42 | public async Task MetadataMiddlewareShouldRegisterFunctionWhenNotRegistered() 43 | { 44 | // Arrange 45 | var functionId = "098039840"; 46 | var entryPoint = $"{typeof(FakeFunctionClass).FullName}.TestFunction"; 47 | var metadata = new FunctionsAuthorizationMetadataMiddleware(_options!); 48 | var functionContext = SetupFunctionContext(functionId, "TestFunction", entryPoint, "httpTrigger", "request"); 49 | 50 | // Act 51 | await metadata.Invoke(functionContext, async fc => await Task.CompletedTask); 52 | 53 | // Assert 54 | Assert.AreEqual(2, _options!.Value.AuthorizationMetadata.Count); 55 | Assert.IsTrue(_options!.Value.IsFunctionRegistered("TestFunction")); 56 | } 57 | 58 | [TestMethod("MetadataMiddleware: should not register function when already registered")] 59 | public async Task MetadataMiddlewareShouldNotRegisterFunctionWhenAlreadyRegistered() 60 | { 61 | // Arrange 62 | var functionId = "098039840"; 63 | var entryPoint = $"{typeof(FakeFunctionClass).FullName}.TestFunction"; 64 | var metadata = new FunctionsAuthorizationMetadataMiddleware(_options!); 65 | var functionContext = SetupFunctionContext(functionId, "TestFunction", entryPoint, "httpTrigger", "request"); 66 | 67 | // Act 68 | await metadata.Invoke(functionContext, async fc => await Task.CompletedTask); 69 | await metadata.Invoke(functionContext, async fc => await Task.CompletedTask); 70 | 71 | // Assert 72 | Assert.AreEqual(2, _options!.Value.AuthorizationMetadata.Count); 73 | Assert.IsTrue(_options!.Value.IsFunctionRegistered("TestFunction")); 74 | } 75 | 76 | [TestMethod("MetadataMiddleware: should throw when defining function with internal method")] 77 | public async Task MetadataMiddlewareShouldThrowWhenDefiningFunctionWithInternalMethod() 78 | { 79 | // Arrange 80 | var functionId = "098039841"; 81 | var entryPoint = $"{typeof(FakeFunctionClass).FullName}.TestFunction2"; 82 | var metadata = new FunctionsAuthorizationMetadataMiddleware(_options!); 83 | var functionContext = SetupFunctionContext(functionId, "TestFunction2", entryPoint, "httpTrigger", "request"); 84 | 85 | // Act 86 | var exception = await Assert.ThrowsExceptionAsync( 87 | async () => await metadata.Invoke(functionContext, async fc => await Task.CompletedTask)); 88 | 89 | // Assert 90 | Assert.IsNotNull(exception); 91 | } 92 | 93 | private FunctionContext SetupFunctionContext(string functionId, string functionName, string entryPoint, string triggerType, string boundTriggerParamName) 94 | { 95 | var binding = new Mock(); 96 | binding.SetupGet(binding => binding.Type).Returns(triggerType); 97 | 98 | var bindingsBuilder = ImmutableDictionary.CreateBuilder(); 99 | bindingsBuilder.Add(boundTriggerParamName, binding.Object); 100 | 101 | var context = new Mock(); 102 | context.Setup(context => context.FunctionId).Returns(functionId); 103 | context.Setup(context => context.FunctionDefinition.Name).Returns(functionName); 104 | context.Setup(context => context.FunctionDefinition.EntryPoint).Returns(entryPoint); 105 | context.Setup(context => context.Features).Returns(Mock.Of()); 106 | context 107 | .Setup(contextMock => contextMock.FunctionDefinition.InputBindings) 108 | .Returns(bindingsBuilder.ToImmutable()); 109 | return context.Object; 110 | } 111 | } 112 | } 113 | --------------------------------------------------------------------------------