├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md └── src ├── ApiAuthenticationService.cs ├── ApiAuthorizationResult.cs ├── AuthorizationHeaderBearerTokenExtractor.cs ├── AzureFunctions.OidcAuthentication.csproj ├── AzureFunctions.OidcAuthentication.nuspec ├── HealthCheckResult.cs ├── IApiAuthentication.cs ├── IAuthorizationHeaderBearerTokenExtractor.cs ├── IJwtSecurityTokenHandlerWrapper.cs ├── IOidcConfigurationManager.cs ├── JwtSecurityTokenHandlerWrapper.cs ├── OidcApiAuthorizationSettings.cs ├── OidcConfigurationManager.cs ├── ServicesConfigurationExtensions.cs └── icon.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Setup .NET Core 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 3.1.404 23 | - name: Build with dotnet 24 | run: dotnet build --configuration Release 25 | working-directory: .\src 26 | # - name: Test 27 | # run: dotnet test 28 | # working-directory: .\src 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Nuget 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 3.1.404 18 | - name: Create NuGet Package 19 | run: dotnet pack -c Release /p:Version=${{ github.event.release.tag_name }} /p:PackageReleaseNotes="See https://github.com/AspNetMonsters/AzureFunctions.OidcAuthentication/releases/tag/${{ github.event.release.tag_name }}" 20 | working-directory: .\src 21 | - name: Archive NuGet Package 22 | uses: actions/upload-artifact@v1 23 | with: 24 | name: AzureFunctions.OidcAuthentication.${{ github.event.release.tag_name }}.nupkg 25 | path: .\src\bin\Release\AzureFunctions.OidcAuthentication.${{ github.event.release.tag_name }}.nupkg 26 | - name: Publish to Nuget.org 27 | run: dotnet nuget push .\src\bin\Release\AzureFunctions.OidcAuthentication.${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ASP.NET Monsters 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureFunctions.OidcAuthentication 2 | [![Build status](https://github.com/AspNetMonsters/AzureFunctions.OidcAuthentication/workflows/Continuous%20Integration/badge.svg)](https://github.com/AspNetMonsters/AzureFunctions.OidcAuthentication/actions?query=workflow%3A%22Continuous+Integration%22) [![NuGet Badge](https://buildstats.info/nuget/AzureFunctions.OidcAuthentication)](https://www.nuget.org/packages/AzureFunctions.OidcAuthentication/) 3 | 4 | > This project is originally forked from https://github.com/bryanknox/AzureFunctionsOpenIDConnectAuthSample. Thank you to Bryan for the helpful sample. 5 | 6 | ## Why? 7 | As of writing this, securing Azure Functions using Bearer token is clumsy. For some auth providers, you can enable App Service Authentication in the Azure Portal but that only works for the deployed version of your app which makes testing locally difficult and clumsy. 8 | 9 | This library makes it easy to authenticate a user by validating a bearer token. 10 | 11 | ## Requirements 12 | 13 | Azure Functions v3 14 | Dependency Injection using Azure Functions Extensions 15 | An identity provider (e.g. Auth0, Azure AD, Okta) 16 | 17 | ## How to use it 18 | 19 | First, configure dependency injection for your Azure Functions project. Start by adding the Microsoft.Azure.Functions.Exentions NuGet package. 20 | 21 | > dotnet add package Microsoft.Azure.Functions.Extensions 22 | 23 | Add the OidcAuthentication NuGet package to your Azure Functions project. 24 | 25 | > dotnet package install AzureFunctions.OidcAuthentication 26 | 27 | Now add a `FunctionStartup` class to configure services that will be used in your Azure Functions app. In the `Configure` method, add a call to `builder.Services.AddOidcApiAuthorization();` This will configure the `IApiAuthentication` service that you will use to authenticate users. 28 | 29 | [assembly: FunctionsStartup(typeof(Curbsy.API.Startup.DependencyInjection))] 30 | 31 | ``` 32 | namespace MySecuredApp 33 | { 34 | public class Startup : FunctionsStartup 35 | { 36 | public override void Configure(IFunctionsHostBuilder builder) 37 | { 38 | builder.Services.AddOidcApiAuthorization(); 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | Configuration is loaded from a environment variables which can be set in `local.settings.json` for local development or in the Azure portal for your deployed app. Settings are prefixed with `OidcApiAuthSettings:`. 45 | 46 | Here is an example `local.settings.json` file for Azure AD B2C: 47 | 48 | ``` 49 | { 50 | "IsEncrypted": false, 51 | "Values": { 52 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 53 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 54 | "OidcApiAuthSettings:Audience": "Your API Application's Client ID", 55 | "OidcApiAuthSettings:MetadataAddress": "https://yourb2cdomain.b2clogin.com/yourb2cdomain.onmicrosoft.com/yoursigninuserflowname/v2.0/.well-known/openid-configuration/", 56 | "OidcApiAuthSettings:IssuerUrl": "https://yourb2cdomain.b2clogin.com/Your Directory (tenant) ID/v2.0/" 57 | } 58 | } 59 | ``` 60 | 61 | ### Settings 62 | 63 | **OidcApiAuthSettings:Audience** - Required 64 | 65 | Identifies the API to be authorized by the Open ID Connect provider (issuer). 66 | 67 | The "Audience" is the identifer used by the authorization provider to identify the API (HTTP triggered Azure Function) being protected. This is often a URL but it is not used as a URL is is simply used as an identifier. 68 | 69 | For Auth0 use the API's Identifier in the Auth0 Dashboard. 70 | 71 | For Azure AD B2C, use your API Application's (client) ID. This is a GUID. 72 | 73 | **OidcApiAuthSettings:IssuerUrl** - Required 74 | 75 | The URL of the Open ID Connect provider (issuer) that will perform API authorization. 76 | 77 | The "Issuer" is the URL for the authorization provider's end-point. This URL will be used as part of the OpenID Connect protocol to obtain the the signing keys that will be used to validate the JWT Bearer tokens in incoming HTTP request headers. 78 | 79 | For Auth0 the URL format is: `https://{Auth0-tenant-domain}.auth0.com` 80 | For Auzre AD B2C, the format is: `https://yourb2cdomain.b2clogin.com/Your Directory (tenant) ID/v2.0/` 81 | 82 | **OidcApiAuthSettings:MetadataAddress** - Optional (depending on identity provider) 83 | 84 | The URL for the identity provider's well-known openid-configuration url. 85 | 86 | Default Vaule: `$"{IssuerUrl}.well-known/openid-configuration"` 87 | 88 | For Auth0, leave this blank. 89 | For Azure AD B2C, use `https://yourb2cdomain.b2clogin.com/yourb2cdomain.onmicrosoft.com/yoursigninuserflowname/v2.0/.well-known/openid-configuration/` 90 | 91 | **OidcApiAuthSettings:NameClaimType** - Optional 92 | 93 | A string defining the name of the claim that will identify the user's name 94 | 95 | Default value: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` 96 | 97 | **OidcApiAuthSettings:RoleClaimType** - Optional 98 | 99 | A string defining the name of the claim that will identify the user's role membership 100 | 101 | Default value: "http://schemas.microsoft.com/ws/2008/06/identity/claims/roleidentifier" 102 | 103 | 104 | ### Securing an Azure Function 105 | Not that everything is configured, you can inject the `IApiAuthentication` service into your Azure Function and authenticate users as follows: 106 | 107 | ``` 108 | namespace MySecuredApp 109 | { 110 | public class MyFunction 111 | { 112 | private readonly IApiAuthentication apiAuthentication; 113 | 114 | public MyFunction(IApiAuthentication apiAuthentication) 115 | { 116 | this.apiAuthentication = apiAuthentication; 117 | } 118 | 119 | [FunctionName("MyFunction")] 120 | public async Task Run( 121 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 122 | ILogger log) 123 | { 124 | // Authenticate the user 125 | var authResult = await this.apiAuthentication.AuthenticateAsync(req.Headers); 126 | 127 | // Check the authentication result 128 | if (authResult.Failed) 129 | { 130 | return new ForbidResult(authenticationScheme: "Bearer"); 131 | } 132 | 133 | // User is authenticated. Proceed with function logic 134 | string name = authResult.User.Identity.Name; // This gives us the unique user name 135 | 136 | string responseMessage = $"Hello, {name}. This HTTP triggered function executed successfully."; 137 | 138 | return new OkObjectResult(responseMessage); 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | `AuthResult.User` is a [ClaimsPrincipal](https://docs.microsoft.com/dotnet/api/system.security.claims.claimsprincipal) that is created using the claims that were included in the JWT token that was validated by the `IApiAuthentication` service. You can use `authResult.User` to inspect the user's claims and add your own authorization rules inside your Function. 145 | 146 | ## End-to-end Sample 147 | - **Functions App:** https://github.com/AspNetMonsters/functions-azure-b2c-sample 148 | - **Vue front-end:** https://github.com/AspNetMonsters/vue-azure-b2c-sample 149 | -------------------------------------------------------------------------------- /src/ApiAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Options; 7 | using Microsoft.IdentityModel.Tokens; 8 | 9 | namespace AzureFunctions.OidcAuthentication 10 | { 11 | /// 12 | /// Encapsulates checks of bearer tokens in HTTP request headers. 13 | /// 14 | internal class ApiAuthenticationService : IApiAuthentication 15 | { 16 | private readonly IAuthorizationHeaderBearerTokenExtractor _authorizationHeaderBearerTokenExractor; 17 | 18 | private readonly IJwtSecurityTokenHandlerWrapper _jwtSecurityTokenHandlerWrapper; 19 | 20 | private readonly IOidcConfigurationManager _oidcConfigurationManager; 21 | 22 | private readonly string _issuerUrl; 23 | private readonly string _issuer; 24 | private readonly string _audience; 25 | 26 | private readonly string _nameClaimType; 27 | private readonly string _roleClaimType; 28 | 29 | public ApiAuthenticationService( 30 | IOptions apiAuthorizationSettingsOptions, 31 | IAuthorizationHeaderBearerTokenExtractor authorizationHeaderBearerTokenExractor, 32 | IJwtSecurityTokenHandlerWrapper jwtSecurityTokenHandlerWrapper, 33 | IOidcConfigurationManager oidcConfigurationManager) 34 | { 35 | _issuerUrl = apiAuthorizationSettingsOptions?.Value?.IssuerUrl; 36 | _issuer = apiAuthorizationSettingsOptions?.Value?.Issuer ?? _issuerUrl; 37 | _audience = apiAuthorizationSettingsOptions?.Value?.Audience; 38 | _nameClaimType = apiAuthorizationSettingsOptions?.Value?.NameClaimType ?? "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; 39 | _roleClaimType = apiAuthorizationSettingsOptions?.Value?.RoleClaimType ?? "http://schemas.microsoft.com/ws/2008/06/identity/claims/roleidentifier"; 40 | 41 | _authorizationHeaderBearerTokenExractor = authorizationHeaderBearerTokenExractor; 42 | 43 | _jwtSecurityTokenHandlerWrapper = jwtSecurityTokenHandlerWrapper; 44 | 45 | _oidcConfigurationManager = oidcConfigurationManager; 46 | } 47 | 48 | /// 49 | /// Checks the given HTTP request headers for a valid OpenID Connect (OIDC) Authorization token. 50 | /// 51 | /// 52 | /// The HTTP request headers to check. 53 | /// 54 | /// 55 | /// Information about the success or failure of the authorization. 56 | /// 57 | public async Task AuthenticateAsync( 58 | IHeaderDictionary httpRequestHeaders) 59 | { 60 | bool isTokenValid = false; 61 | ClaimsPrincipal principal = new ClaimsPrincipal(); 62 | 63 | string authorizationBearerToken = _authorizationHeaderBearerTokenExractor.GetToken( 64 | httpRequestHeaders); 65 | if (authorizationBearerToken == null) 66 | { 67 | return new ApiAuthenticationResult(principal, 68 | "Authorization header is missing, invalid format, or is not a Bearer token."); 69 | } 70 | 71 | int validationRetryCount = 0; 72 | 73 | do 74 | { 75 | IEnumerable isserSigningKeys; 76 | try 77 | { 78 | // Get the cached signing keys if they were retrieved previously. 79 | // If they haven't been retrieved, or the cached keys are stale, 80 | // then a fresh set of signing keys are retrieved from the OpenID Connect provider 81 | // (issuer) cached and returned. 82 | // This method will throw if the configuration cannot be retrieved, instead of returning null. 83 | isserSigningKeys = await _oidcConfigurationManager.GetIssuerSigningKeysAsync(); 84 | } 85 | catch (Exception ex) 86 | { 87 | return new ApiAuthenticationResult(principal, 88 | "Problem getting signing keys from Open ID Connect provider (issuer)." 89 | + $" ConfigurationManager threw {ex.GetType()} Message: {ex.Message}"); 90 | } 91 | 92 | try 93 | { 94 | // Try to validate the token. 95 | 96 | var tokenValidationParameters = new TokenValidationParameters 97 | { 98 | RequireSignedTokens = true, 99 | ValidAudience = _audience, 100 | ValidateAudience = true, 101 | ValidIssuer = _issuer, 102 | ValidateIssuer = true, 103 | ValidateIssuerSigningKey = true, 104 | ValidateLifetime = true, 105 | IssuerSigningKeys = isserSigningKeys, 106 | NameClaimType = _nameClaimType, 107 | RoleClaimType = _roleClaimType 108 | }; 109 | 110 | // Throws if the the token cannot be validated. 111 | principal = _jwtSecurityTokenHandlerWrapper.ValidateToken( 112 | authorizationBearerToken, 113 | tokenValidationParameters); 114 | 115 | isTokenValid = true; 116 | } 117 | catch (SecurityTokenSignatureKeyNotFoundException) 118 | { 119 | // A SecurityTokenSignatureKeyNotFoundException is thrown if the signing keys for 120 | // validating the JWT could not be found. This could happen if the issuer has 121 | // changed the signing keys since the last time they were retrieved by the 122 | // ConfigurationManager. To handle this we ask the ConfigurationManger to refresh 123 | // which causes it to retrieve the keys again the next time we ask for them. 124 | // Then we retry by asking for the signing keys and validating the token again. 125 | // We only retry once. 126 | 127 | _oidcConfigurationManager.RequestRefresh(); 128 | 129 | validationRetryCount++; 130 | } 131 | catch (Exception ex) 132 | { 133 | return new ApiAuthenticationResult(principal, 134 | $"Authorization Failed. {ex.GetType()} caught while validating JWT token." 135 | + $"Message: {ex.Message}"); 136 | } 137 | 138 | } while (!isTokenValid && validationRetryCount <= 1); 139 | 140 | // Success result. 141 | return new ApiAuthenticationResult(principal); 142 | } 143 | 144 | public async Task HealthCheckAsync() 145 | { 146 | if (string.IsNullOrWhiteSpace(_audience) 147 | || string.IsNullOrWhiteSpace(_issuerUrl)) 148 | { 149 | return new HealthCheckResult( 150 | $"Some or all {nameof(OidcApiAuthSettings)} are missing."); 151 | } 152 | 153 | try 154 | { 155 | // Get the singing keys fresh. Not from the cache. 156 | _oidcConfigurationManager.RequestRefresh(); 157 | 158 | await _oidcConfigurationManager.GetIssuerSigningKeysAsync(); 159 | } 160 | catch (Exception ex) 161 | { 162 | return new HealthCheckResult( 163 | "Problem getting signing keys from Open ID Connect provider (issuer)." 164 | + $" ConfigurationManager threw {ex.GetType()} Message: {ex.Message}"); 165 | } 166 | 167 | return new HealthCheckResult(); // Good health. 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/ApiAuthorizationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace AzureFunctions.OidcAuthentication 4 | { 5 | /// 6 | /// Encapsulates the results of an API authorization. 7 | /// 8 | public class ApiAuthenticationResult 9 | { 10 | /// 11 | /// Constructs a success authorization. 12 | /// 13 | public ApiAuthenticationResult(ClaimsPrincipal principal) 14 | { 15 | User = principal; 16 | } 17 | 18 | /// 19 | /// Constructs a failed authorization with given reason. 20 | /// 21 | /// 22 | /// Describes the reason for the authorization failure. 23 | /// 24 | public ApiAuthenticationResult(ClaimsPrincipal principal, string failureReason) 25 | { 26 | User = principal; 27 | FailureReason = failureReason; 28 | } 29 | 30 | /// 31 | /// True if authorization failed. 32 | /// 33 | public bool Failed => FailureReason != null; 34 | 35 | /// 36 | /// String describing the reason for the authorization failure. 37 | /// 38 | public string FailureReason { get; } 39 | 40 | public ClaimsPrincipal User { get; } 41 | 42 | /// 43 | /// True if authorization was successful. 44 | /// 45 | public bool Success => !Failed; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AuthorizationHeaderBearerTokenExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http.Headers; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace AzureFunctions.OidcAuthentication 8 | { 9 | internal class AuthorizationHeaderBearerTokenExtractor : IAuthorizationHeaderBearerTokenExtractor 10 | { 11 | /// 12 | /// Extracts the Bearer token from the Authorization header of the given HTTP request headers. 13 | /// 14 | /// 15 | /// The headers from an HTTP request. 16 | /// 17 | /// 18 | /// The Bearer token extracted from the Authorization header (without the "Bearer " prefix), 19 | /// or null if the Authorization header was not found, it is in an invalid format, 20 | /// or its value is not a Bearer token. 21 | /// 22 | public string GetToken(IHeaderDictionary httpRequestHeaders) 23 | { 24 | // Get a StringValues object that represents the content of the Authorization header found in the given 25 | // headers. 26 | // Note that the default for a IHeaderDictionary is a StringValues object with one null string. 27 | StringValues rawAuthorizationHeaderValue = httpRequestHeaders 28 | .SingleOrDefault(x => x.Key == "Authorization") // Case sensitive. 29 | .Value; 30 | 31 | if (rawAuthorizationHeaderValue.Count != 1) 32 | { 33 | // StringValues' Count will be zero if there is no Authorization header 34 | // and greater than one if there are more than one Authorization headers. 35 | return null; 36 | } 37 | 38 | // We got a value from the Authorization header. 39 | 40 | if (!AuthenticationHeaderValue.TryParse( 41 | rawAuthorizationHeaderValue, // StringValues automatically convert to string. 42 | out AuthenticationHeaderValue authenticationHeaderValue)) 43 | { 44 | // Invalid token format. 45 | return null; 46 | } 47 | 48 | if (!string.Equals( 49 | authenticationHeaderValue.Scheme, 50 | "Bearer", 51 | StringComparison.InvariantCultureIgnoreCase)) // Case insenitive. 52 | { 53 | // The Authorization header's value is not a Bearer token. 54 | return null; 55 | } 56 | 57 | // Return the token from the Athorization header. 58 | // This is the token with the "Bearer " prefix removed. 59 | // The Parameter will be null, if nothing followed the "Bearer " prefix. 60 | return authenticationHeaderValue.Parameter; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AzureFunctions.OidcAuthentication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | ASP.NET Monsters 6 | AzureFunctions.OidcAuthentication 7 | 1.0.0.0 8 | ASP.NET Monsters 2020 9 | Azure Functions OIDC Authentiction 10 | Easily validate OIDC / JWT Bearer Tokens to authenticate users and secure your Azure Functions application. Works with popular identity providers including Auth0, Azure AD B2C, Azure AD, and Okta. 11 | David Paquette 12 | git 13 | git://github.com/AspNetMonsters/AzureFunctions.OidcAuthentication 14 | $(MSBuildThisFileDirectory)$(MSBuildProjectName).nuspec 15 | 16 | 17 | 18 | $(NuspecProperties);id=$(AssemblyName) 19 | $(NuspecProperties);config=$(Configuration) 20 | $(NuspecProperties);version=$(PackageVersion) 21 | $(NuspecProperties);description=$(Description) 22 | $(NuspecProperties);authors=$(Authors) 23 | $(NuspecProperties);releaseNotes=$(PackageReleaseNotes) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/AzureFunctions.OidcAuthentication.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $authors$ 7 | false 8 | MIT 9 | $description$ 10 | Azure Functions;Token Validation;JWT;OIDC;Auth0;Azure AD B2C;Azure AD;Okta 11 | images\icon.png 12 | $releaseNotes$ 13 | https://github.com/AspNetMonsters/AzureFunctions.OidcAuthentication 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/HealthCheckResult.cs: -------------------------------------------------------------------------------- 1 | namespace AzureFunctions.OidcAuthentication 2 | { 3 | public class HealthCheckResult 4 | { 5 | /// 6 | /// Construt a HealthCheckResult that indicates good health. 7 | /// 8 | public HealthCheckResult() 9 | { 10 | } 11 | 12 | /// 13 | /// Construt a HealthCheckResult that indicates bad health. 14 | /// 15 | /// 16 | /// The message describing the bad health. 17 | /// 18 | public HealthCheckResult(string badHealthMessage) 19 | { 20 | BadHealthMessage = badHealthMessage; 21 | } 22 | 23 | public bool IsHealthy => BadHealthMessage == null; 24 | 25 | public string BadHealthMessage { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/IApiAuthentication.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace AzureFunctions.OidcAuthentication 5 | { 6 | public interface IApiAuthentication 7 | { 8 | Task AuthenticateAsync(IHeaderDictionary httpRequestHeaders); 9 | Task HealthCheckAsync(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/IAuthorizationHeaderBearerTokenExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace AzureFunctions.OidcAuthentication 4 | { 5 | internal interface IAuthorizationHeaderBearerTokenExtractor 6 | { 7 | /// 8 | /// Extract the Bearer token from the Authorization header of the given HTTP request headers. 9 | /// 10 | /// 11 | /// The headers from an HTTP request. 12 | /// 13 | /// 14 | /// The Bearer token extracted from the Authorization header, 15 | /// or null if the authorization header was not found 16 | /// or its value is not a Bearer token. 17 | /// 18 | string GetToken(IHeaderDictionary httpRequestHeaders); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IJwtSecurityTokenHandlerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.IdentityModel.Tokens; 3 | 4 | namespace AzureFunctions.OidcAuthentication 5 | { 6 | internal interface IJwtSecurityTokenHandlerWrapper 7 | { 8 | ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/IOidcConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.IdentityModel.Tokens; 4 | 5 | namespace AzureFunctions.OidcAuthentication 6 | { 7 | internal interface IOidcConfigurationManager 8 | { 9 | /// 10 | /// Returns the cached signing keys if they were retrieved previously. 11 | /// If they haven't been retrieved, or the cached keys are stale, then a fresh set of 12 | /// signing keys are retrieved from the OpenID Connect provider (issuer) cached and returned. 13 | /// This method will throw if the configuration cannot be retrieved, instead of returning null. 14 | /// 15 | /// 16 | /// The current set of the Open ID Connect issuer's signing keys. 17 | /// 18 | Task> GetIssuerSigningKeysAsync(); 19 | 20 | /// 21 | /// Requests that the next call to GetConfigurationAsync() obtain new configuration. 22 | /// 23 | void RequestRefresh(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/JwtSecurityTokenHandlerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | using Microsoft.IdentityModel.Tokens; 4 | 5 | namespace AzureFunctions.OidcAuthentication 6 | { 7 | internal class JwtSecurityTokenHandlerWrapper : IJwtSecurityTokenHandlerWrapper 8 | { 9 | /// 10 | /// Reads and validates a 'JSON Web Token' (JWT) and throws an exception if 11 | /// the token could not be validated. 12 | /// 13 | /// 14 | /// A JSON Web Token (JWT) encoded as a JWS or JWE in Compact Serialized Format. 15 | /// 16 | /// 17 | /// Contains parameters used in the validation of the token. 18 | /// 19 | public ClaimsPrincipal ValidateToken( 20 | string token, 21 | TokenValidationParameters tokenValidationParameters) 22 | { 23 | var handler = new JwtSecurityTokenHandler(); 24 | 25 | // Try to validate the token. 26 | // Throws if the the token cannot be validated. 27 | // We don't need the ClaimsPrincipal that is returned. 28 | return handler.ValidateToken( 29 | token, 30 | tokenValidationParameters, 31 | out _); // Discard the output SecurityToken. We don't need it. 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/OidcApiAuthorizationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace AzureFunctions.OidcAuthentication 2 | { 3 | /// 4 | /// Encapsulates settings used in OpenID Connect (OIDC) API authentication. 5 | /// 6 | public class OidcApiAuthSettings 7 | { 8 | private string _issuerUrl; 9 | 10 | /// 11 | /// Identifies the API to be authorized by the Open ID Connect provider (issuer). 12 | /// 13 | /// 14 | /// The "Audience" is the identifer used by the authorization provider to identify 15 | /// the API (HTTP triggered Azure Function) being protected. This is often a URL but 16 | /// it is not used as a URL is is simply used as an identifier. 17 | /// 18 | /// For Auth0 use the API's Identifier in the Auth0 Dashboard. 19 | /// 20 | public string Audience { get; set; } 21 | 22 | /// 23 | /// The URL of the Open ID Connect provider (issuer) that will perform API authorization. 24 | /// 25 | /// 26 | /// The "Issuer" is the URL for the authorization provider's end-point. This URL will be 27 | /// used as part of the OpenID Connect protocol to obtain the the signing keys 28 | /// that will be used to validate the JWT Bearer tokens in incoming HTTP request headers. 29 | /// 30 | /// For Auth0 the URL format is: https://{Auth0-tenant-domain}.auth0.com 31 | /// 32 | public string IssuerUrl 33 | { 34 | get 35 | { 36 | return _issuerUrl; 37 | } 38 | set 39 | { 40 | if (!string.IsNullOrWhiteSpace(value) && !value.EndsWith("/")) 41 | { 42 | _issuerUrl = value + "/"; 43 | } 44 | else 45 | { 46 | _issuerUrl = value; 47 | } 48 | } 49 | } 50 | 51 | 52 | /// 53 | /// (Optional) The URL for the identity provider's well-known openid-configuration url. 54 | /// Default Vaule: `$"{IssuerUrl}.well-known/openid-configuration"` 55 | /// 56 | /// 57 | /// For Auth0, leave this blank. 58 | /// For Azure AD B2C, use `https://yourb2cdomain.b2clogin.com/yourb2cdomain.onmicrosoft.com/yoursigninuserflowname/v2.0/.well-known/openid-configuration/` 59 | /// 60 | public string MetadataAddress { get; set; } 61 | 62 | /// 63 | /// (Optional) A string defining the name of the claim that will identify the user's name 64 | /// Default value: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" 65 | /// 66 | public string NameClaimType {get; set;} 67 | 68 | /// 69 | /// (Optional) A string defining the name of the claim that will identify the user's role membership 70 | /// Default value: "http://schemas.microsoft.com/ws/2008/06/identity/claims/roleidentifier" 71 | /// 72 | public string RoleClaimType {get; set;} 73 | 74 | 75 | public string Issuer { get; set; } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/OidcConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Options; 5 | using Microsoft.IdentityModel.Protocols; 6 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 7 | using Microsoft.IdentityModel.Tokens; 8 | 9 | namespace AzureFunctions.OidcAuthentication 10 | { 11 | internal class OidcConfigurationManager : IOidcConfigurationManager 12 | { 13 | private readonly ConfigurationManager _configurationManager; 14 | 15 | /// 16 | /// Construct a ConfigurationManager instance for retreiving and caching OpenIdConnectConfiguration 17 | /// from an Open ID Connect provider (issuer) 18 | /// 19 | public OidcConfigurationManager( 20 | IOptions settingsOptions) 21 | { 22 | string issuerUrl = settingsOptions.Value.IssuerUrl; 23 | string metadataAddress = settingsOptions.Value.MetadataAddress; 24 | 25 | if (string.IsNullOrEmpty(metadataAddress)) { 26 | metadataAddress = $"{issuerUrl}.well-known/openid-configuration"; 27 | } 28 | 29 | var documentRetriever = new HttpDocumentRetriever 30 | { 31 | RequireHttps = metadataAddress.StartsWith("https://") 32 | }; 33 | 34 | // Setup the ConfigurationManager to call the issuer (e.g. Auth0) of the signing keys. 35 | // The ConfigurationManager caches the configuration it receives from the OpenID Connect 36 | // provider (issuer) in order to reduce the number or requests to that provider. 37 | // 38 | // The configuration is not retrieved from the OpenID Connect provider until the first time 39 | // the ConfigurationManager.GetConfigurationAsync() is called below. 40 | _configurationManager = new ConfigurationManager( 41 | metadataAddress, 42 | new OpenIdConnectConfigurationRetriever(), 43 | documentRetriever 44 | ); 45 | } 46 | 47 | /// 48 | /// Returns the cached signing keys if they were retrieved previously. 49 | /// If they haven't been retrieved, or the cached keys are stale, then a fresh set of 50 | /// signing keys are retrieved from the OpenID Connect provider (issuer) cached and returned. 51 | /// This method will throw if the configuration cannot be retrieved, instead of returning null. 52 | /// 53 | /// 54 | /// The current set of the Open ID Connect issuer's signing keys. 55 | /// 56 | public async Task> GetIssuerSigningKeysAsync() 57 | { 58 | OpenIdConnectConfiguration configuration = await _configurationManager.GetConfigurationAsync( 59 | CancellationToken.None); 60 | 61 | return configuration.SigningKeys; 62 | } 63 | 64 | /// 65 | /// Requests that the next call to GetIssuerSigningKeysAsync() obtain new signing keys. 66 | /// If the last refresh was greater than RefreshInterval then the next call to 67 | /// GetIssuerSigningKeysAsync() will retrieve new configuration (signing keys). 68 | /// If RefreshInterval == System.TimeSpan.MaxValue then this method does nothing. 69 | /// 70 | /// 71 | /// RefreshInterval defaults to 30 seconds (00:00:30). 72 | /// 73 | public void RequestRefresh() 74 | { 75 | _configurationManager.RequestRefresh(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ServicesConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace AzureFunctions.OidcAuthentication 5 | { 6 | public static class ServicesConfigurationExtensions 7 | { 8 | public static void AddOidcApiAuthorization(this IServiceCollection services) 9 | { 10 | // Setup injection of OidcApiAuthorizationSettings configured in the 11 | // Function's app settings (or local.settings.json) 12 | // as IOptions. 13 | // See https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#working-with-options-and-settings 14 | services.AddOptions() 15 | .Configure((settings, configuration) => 16 | { 17 | configuration.GetSection(nameof(OidcApiAuthSettings)).Bind(settings); 18 | }); 19 | 20 | // These are created as a singletons, so that only one instance of each 21 | // is created for the lifetime of the hosting Azure Function App. 22 | // That helps reduce the number of calls to the authorization service 23 | // for the signing keys and other stuff that can be used across multiple 24 | // calls to the HTTP triggered Azure Functions. 25 | 26 | services.AddSingleton(); 27 | services.AddSingleton(); 28 | services.AddSingleton(); 29 | 30 | services.AddSingleton(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AspNetMonsters/AzureFunctions.OidcAuthentication/5155701c70e7439f2d59f1e8b41f86291a341d2b/src/icon.png --------------------------------------------------------------------------------