├── Functions ├── host.json ├── local.settings.json ├── KeyBindingTester.cs ├── Functions.csproj └── SecretBindingTester.cs ├── Binding ├── Secret │ ├── KeyVaultSecretWebJobsStartup.cs │ ├── KeyVaultSecretExtensions.cs │ ├── KeyVaultSecretAttribute.cs │ └── KeyVaultSecretConfiguration.cs ├── KeyVaultExtensionWebJobsStartup.cs ├── Key │ ├── KeyVaultKeyExtensions.cs │ ├── KeyVaultKeyAttribute.cs │ └── KeyVaultKeyConfiguration.cs └── Functions.Extensions.KeyVault.csproj ├── readme.md ├── AzureFunctionsKeyvaultBinding.sln ├── .gitattributes └── .gitignore /Functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /Functions/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "AzureWebJobsDashboard": "UseDevelopmentStorage=true", 6 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 7 | "MyKv": "", 8 | "MyKeyId": "", 9 | "MySecretId": "" 10 | } 11 | } -------------------------------------------------------------------------------- /Binding/Secret/KeyVaultSecretWebJobsStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Azure.WebJobs.Hosting; 3 | 4 | 5 | namespace Functions.Extensions.KeyVault.Secret 6 | { 7 | /// 8 | /// 9 | public class KeyVaultSecretWebJobsStartup : IWebJobsStartup 10 | { 11 | /// 12 | /// Configures the specified builder. 13 | /// 14 | /// The builder. 15 | public void Configure(IWebJobsBuilder builder) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Binding/KeyVaultExtensionWebJobsStartup.cs: -------------------------------------------------------------------------------- 1 | using Functions.Extensions.KeyVault.Key; 2 | using Functions.Extensions.KeyVault.Secret; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Hosting; 5 | 6 | [assembly: WebJobsStartup(typeof(Functions.Extensions.KeyVault.KeyVaultExtensionWebJobsStartup))] 7 | 8 | namespace Functions.Extensions.KeyVault 9 | { 10 | class KeyVaultExtensionWebJobsStartup : IWebJobsStartup 11 | { 12 | public void Configure(IWebJobsBuilder builder) 13 | { 14 | builder 15 | .AddKeyVaultSecret() 16 | .AddKeyVaultKey(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Functions/KeyBindingTester.cs: -------------------------------------------------------------------------------- 1 | using Functions.Extensions.KeyVault; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.KeyVault.WebKey; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace TestFunctions 10 | { 11 | public static class KeyBindingTester 12 | { 13 | /* 14 | * You can easily test this by GET /GetKey 15 | */ 16 | 17 | [FunctionName(nameof(GetKey))] 18 | public static IActionResult GetKey([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req, [KeyVaultKey(@"MyKv", @"MyKeyId")]JsonWebKey kvKey, ILogger log) 19 | { 20 | return new OkObjectResult($@"Key value: {kvKey.ToString()}"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Functions/Functions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | v2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | Never 20 | 21 | 22 | -------------------------------------------------------------------------------- /Binding/Key/KeyVaultKeyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Azure.WebJobs; 3 | 4 | namespace Functions.Extensions.KeyVault.Key 5 | { 6 | /// 7 | public static class KeyVaultKeyExtensions 8 | { 9 | /// 10 | /// Adds the KeyVaultSecret extension to the provided . 11 | /// 12 | /// The to configure. 13 | /// 14 | /// builder 15 | public static IWebJobsBuilder AddKeyVaultKey(this IWebJobsBuilder builder) 16 | { 17 | if (builder == null) 18 | { 19 | throw new ArgumentNullException(nameof(builder)); 20 | } 21 | 22 | builder 23 | .AddExtension(); 24 | 25 | return builder; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Binding/Secret/KeyVaultSecretExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Azure.WebJobs; 3 | 4 | namespace Functions.Extensions.KeyVault.Secret 5 | { 6 | /// 7 | public static class KeyVaultSecretExtensions 8 | { 9 | /// 10 | /// Adds the KeyVaultSecret extension to the provided . 11 | /// 12 | /// The to configure. 13 | /// 14 | /// builder 15 | public static IWebJobsBuilder AddKeyVaultSecret(this IWebJobsBuilder builder) 16 | { 17 | if (builder == null) 18 | { 19 | throw new ArgumentNullException(nameof(builder)); 20 | } 21 | 22 | builder 23 | .AddExtension(); 24 | 25 | return builder; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Binding/Functions.Extensions.KeyVault.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Functions.Extensions.KeyVault 6 | 7 | 8 | 9 | bin\Release\netstandard2.0\Functions.Extensions.KeyVault.xml 10 | pdbonly 11 | true 12 | 13 | 14 | 15 | bin\Debug\netstandard2.0\Functions.Extensions.KeyVault.xml 16 | full 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Functions/SecretBindingTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Functions.Extensions.KeyVault; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.Http; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace TestFunctions 11 | { 12 | public static class SecretBindingTester 13 | { 14 | /* 15 | * You can easily test this by first GET /GetSecret, then POST to the /SetSecret function a new value (no decorating, just the string to set it to) then GET /GetSecret again 16 | */ 17 | 18 | [FunctionName(nameof(GetSecret))] 19 | public static IActionResult GetSecret([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req, [KeyVaultSecret(@"MyKv", @"MySecretId")]string secretValue, ILogger log) 20 | { 21 | return new OkObjectResult($@"Secret: {secretValue}"); 22 | } 23 | 24 | [FunctionName(nameof(SetSecret))] 25 | public static IActionResult SetSecret([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, [KeyVaultSecret(@"MyKv", @"MySecretId")]out string secretValue, ILogger log) 26 | { 27 | // can't use the async overload here & async/await, because async methods can't have 'out' params. 28 | // This output binding doesn't make sense to use with an IAsyncCollector because why would you set the same secret's value multiple times throughout a run? 29 | secretValue = new StreamReader(req.Body).ReadToEnd(); 30 | 31 | return new OkObjectResult($@"[{Environment.GetEnvironmentVariable(@"MySecretId")}]: {secretValue}"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://bc3tech.visualstudio.com/Projects/_apis/build/status/KeyVault%20Binding%20CI)](https://bc3tech.visualstudio.com/Projects/_build/latest?definitionId=38) 2 | 3 | # An Azure Functions (2.0) binding for KeyVault 4 | 1. Create a new Azure Function instance in Azure 5 | 1. Create a new KeyVault instance in Azure 6 | 1. Ensure the Azure Function has 'Managed Service Identity' turned on 7 | 1. Add the Azure Function (by resource name) to the Key Vault's Access Policy list with 'Secret | Get' permissions 8 | Fill out only the 'Select Principal' part, not the 'Authorized application' part of the form 9 | 10 | You can get more detail on setting this up by reading [this blog post from Functions PM, Jeff Hollan](https://medium.com/statuscode/getting-key-vault-secrets-in-azure-functions-37620fd20a0b). 11 | 1. Use the KeyVault binding in your Azure Function by: 12 | 13 | Adding the nuget package to your project 14 | 15 | ~~~ 16 | Install-Package BC3Technologies.Azure.Functions.Extensions.KeyVault -IncludePrerelease 17 | ~~~ 18 | 19 | Then referencing it in your Function definition 20 | 21 | ```csharp 22 | public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req, [KeyVaultSecret(@"MyKv", @"MySecretId")]string secretValue, ILogger log) 23 | ``` 24 | 25 | where `MyKv` and `MySecretId` are defined in your app settings like: 26 | ```json 27 | "MyKv": "kv23958612", 28 | "MySecretId": "fooSecret" 29 | ``` 30 | 31 | 6. Run your function & you will see the `secretValue` parameter populated with the value from the `MyKv` Key Vault for the secret `MySecretId` 32 | 33 | - You can also use `[KeyVaultSecret(@"MyKv", @"MySecretId")]**out** string myNewKeyValue` to _set_ the value of `fooSecret` in KeyVault 34 | - You can get a JsonWebKey for a Key Vault **key** by using `[KeyVaultKey("kvresourcesetting","keynamesetting")]JsonWebKey myKey` -------------------------------------------------------------------------------- /AzureFunctionsKeyvaultBinding.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2047 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EBA5E3D9-898A-4BC3-B329-D4FA3423B587}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitignore = .gitignore 9 | readme.md = readme.md 10 | EndProjectSection 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Extensions.KeyVault", "Binding\Functions.Extensions.KeyVault.csproj", "{957EA7D1-30A5-47AB-ACE0-F81CFFBF0836}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions", "Functions\Functions.csproj", "{32C6737F-9871-4B54-8C23-81A7AFFE469C}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {957EA7D1-30A5-47AB-ACE0-F81CFFBF0836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {957EA7D1-30A5-47AB-ACE0-F81CFFBF0836}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {957EA7D1-30A5-47AB-ACE0-F81CFFBF0836}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {957EA7D1-30A5-47AB-ACE0-F81CFFBF0836}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {32C6737F-9871-4B54-8C23-81A7AFFE469C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {32C6737F-9871-4B54-8C23-81A7AFFE469C}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {32C6737F-9871-4B54-8C23-81A7AFFE469C}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {32C6737F-9871-4B54-8C23-81A7AFFE469C}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {F6314A26-9AFC-4D60-A07A-A0C56EE34551} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Binding/Key/KeyVaultKeyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Azure.WebJobs.Description; 3 | 4 | namespace Functions.Extensions.KeyVault 5 | { 6 | /// Enables binding to KeyVault to get and set Key Vault Keys. 7 | /// To retrieve the value of a Key from Key Vault, parameter should be 'JsonWebKey x' 8 | /// 9 | // denote the attribute is a function binding with [Binding] 10 | [Binding, AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] 11 | public class KeyVaultKeyAttribute : Attribute 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The name of the application setting (eg: MyKeyVault) which holds the name of the Key Vault resource in Azure (eg: my-key-vault) 17 | /// The name of the application setting which holds the id of the key whose value should be fetched. 18 | /// You must add the appropriate access policies to your KeyVault instance for your Function App's Managed Service Identity in order for this binding to work. 19 | // I prefer to make *required* inputs to the binding be constructor parameters, that way users can't skip them 20 | public KeyVaultKeyAttribute(string resourceNameSetting, string keyIdSetting) 21 | { 22 | this.ResourceNameSetting = resourceNameSetting; 23 | this.KeyIdSetting = keyIdSetting; 24 | } 25 | 26 | /// 27 | /// Gets the name of the application setting (eg: MyKeyVault) which holds the name of the Key Vault resource in Azure (eg: my-key-vault). 28 | /// Default: "KeyVaultResourceName" 29 | /// 30 | [AppSetting(Default = @"KeyVaultResourceName")] 31 | public string ResourceNameSetting { get; } 32 | 33 | /// 34 | /// Gets the name of the application setting which holds the id of the key whose value should be fetched. 35 | /// 36 | [AppSetting] 37 | public string KeyIdSetting { get; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Binding/Secret/KeyVaultSecretAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Azure.WebJobs.Description; 3 | 4 | namespace Functions.Extensions.KeyVault 5 | { 6 | /// Enables binding to KeyVault to get and set Key Vault Secrets. 7 | /// To retrieve the value of a Secret from Key Vault, parameter should be 'string x' 8 | /// To set the valut of a Key Vault Secret, parameter should be 'out string x' 9 | /// 10 | // denote the attribute is a function binding with [Binding] 11 | [Binding, AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] 12 | public class KeyVaultSecretAttribute : Attribute 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The name of the application setting (eg: MyKeyVault) which holds the name of the Key Vault resource in Azure (eg: my-key-vault) 18 | /// The name of the application setting which holds the id of the secret whose value should be fetched. 19 | /// You must add the appropriate access policies to your KeyVault instance for your Function App's Managed Service Identity in order for this binding to work. 20 | // I prefer to make *required* inputs to the binding be constructor parameters, that way users can't skip them 21 | public KeyVaultSecretAttribute(string resourceNameSetting, string secretIdSetting) 22 | { 23 | this.ResourceNameSetting = resourceNameSetting; 24 | this.SecretIdSetting = secretIdSetting; 25 | } 26 | 27 | /// 28 | /// Gets the name of the application setting (eg: MyKeyVault) which holds the name of the Key Vault resource in Azure (eg: my-key-vault). 29 | /// Default: "KeyVaultResourceName" 30 | /// 31 | [AppSetting(Default = @"KeyVaultResourceName")] 32 | public string ResourceNameSetting { get; } 33 | 34 | /// 35 | /// Gets the name of the application setting which holds the id of the secret whose value should be fetched. 36 | /// 37 | [AppSetting] 38 | public string SecretIdSetting { get; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Binding/Key/KeyVaultKeyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Azure.KeyVault; 6 | using Microsoft.Azure.KeyVault.WebKey; 7 | using Microsoft.Azure.Services.AppAuthentication; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Description; 10 | using Microsoft.Azure.WebJobs.Host.Config; 11 | 12 | namespace Functions.Extensions.KeyVault 13 | { 14 | /// 15 | /// 16 | [Extension(@"KeyVaultKey")] 17 | public class KeyVaultKeyConfiguration : IExtensionConfigProvider 18 | { 19 | // Make these static, particularly the HttpClient, so as not to exhaust the connection pool when using input & output bindings 20 | private static readonly HttpClient _httpClient = new HttpClient(); 21 | private static readonly AzureServiceTokenProvider _tokenProvider = new AzureServiceTokenProvider(); 22 | 23 | private static readonly KeyVaultClient _kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(_tokenProvider.KeyVaultTokenCallback), _httpClient); 24 | 25 | /// 26 | /// Initializes the specified context. 27 | /// 28 | /// The context. 29 | public void Initialize(ExtensionConfigContext context) 30 | { 31 | // Tell the Functions host that we want to add a new binding based on the KeyVaultSecret attribute class 32 | context.AddBindingRule() 33 | // Let funchost know it's an Input binding, and how to convert it to the type the user specifies (eg: string). If you want the user to be able to use other types, you must add more 'BindToInput' calls that return those types as values. Here, I have to use a class implementing the IAsyncConverter because I need to call async methods to perform the conversion 34 | .BindToInput(KeyVaultKeyInputConverter.Instance) 35 | // Add a validator on the user's attribute implementation to make sure I can even do the conversion and blow up accordingly if I can't. 36 | .AddValidator((attrib, t) => 37 | { 38 | if (string.IsNullOrWhiteSpace(attrib.ResourceNameSetting)) 39 | { 40 | throw new ArgumentException(nameof(attrib.ResourceNameSetting)); 41 | } 42 | 43 | if (string.IsNullOrWhiteSpace(attrib.KeyIdSetting)) 44 | { 45 | throw new ArgumentNullException(nameof(attrib.KeyIdSetting)); 46 | } 47 | }); 48 | } 49 | 50 | 51 | class KeyVaultKeyInputConverter : IAsyncConverter 52 | { 53 | private KeyVaultKeyInputConverter() { } 54 | 55 | // Provide a static instance to the keyvault converter so the funchost doesn't have to spin it up over and over, potentially exhausting connections or getting rate-limited 56 | public static KeyVaultKeyInputConverter Instance { get; } = new KeyVaultKeyInputConverter(); 57 | 58 | // "convert" means "take the attribute, and give me back the (in this case string) the user's asking for." So here, it means "go hit the keyvault instance they've specified and get the value for the secret" 59 | public async Task ConvertAsync(KeyVaultKeyAttribute attrib, CancellationToken cancellationToken) 60 | { 61 | var keyBundle = await _kvClient.GetKeyAsync($@"https://{attrib.ResourceNameSetting}.vault.azure.net/keys/{attrib.KeyIdSetting}"); 62 | return keyBundle.Key; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.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 | # Azure Functions publishing profile 8 | *.pubxml 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | 31 | # Visual Studio 2015 cache/options directory 32 | .vs/ 33 | # Uncomment if you have tasks that create the project's static files in wwwroot 34 | #wwwroot/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # DNX 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # TFS 2012 Local Workspace 100 | $tf/ 101 | 102 | # Guidance Automation Toolkit 103 | *.gpState 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper*/ 107 | *.[Rr]e[Ss]harper 108 | *.DotSettings.user 109 | 110 | # JustCode is a .NET coding add-in 111 | .JustCode 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # NCrunch 120 | _NCrunch_* 121 | .*crunch*.local.xml 122 | nCrunchTemp_* 123 | 124 | # MightyMoose 125 | *.mm.* 126 | AutoTest.Net/ 127 | 128 | # Web workbench (sass) 129 | .sass-cache/ 130 | 131 | # Installshield output folder 132 | [Ee]xpress/ 133 | 134 | # DocProject is a documentation generator add-in 135 | DocProject/buildhelp/ 136 | DocProject/Help/*.HxT 137 | DocProject/Help/*.HxC 138 | DocProject/Help/*.hhc 139 | DocProject/Help/*.hhk 140 | DocProject/Help/*.hhp 141 | DocProject/Help/Html2 142 | DocProject/Help/html 143 | 144 | # Click-Once directory 145 | publish/ 146 | 147 | # Publish Web Output 148 | *.[Pp]ublish.xml 149 | *.azurePubxml 150 | # TODO: Comment the next line if you want to checkin your web deploy settings 151 | # but database connection strings (with potential passwords) will be unencrypted 152 | #*.pubxml 153 | *.publishproj 154 | 155 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 156 | # checkin your Azure Web App publish settings, but sensitive information contained 157 | # in these scripts will be unencrypted 158 | PublishScripts/ 159 | 160 | # NuGet Packages 161 | *.nupkg 162 | # The packages folder can be ignored because of Package Restore 163 | **/packages/* 164 | # except build/, which is used as an MSBuild target. 165 | !**/packages/build/ 166 | # Uncomment if necessary however generally it will be regenerated when needed 167 | #!**/packages/repositories.config 168 | # NuGet v3's project.json files produces more ignoreable files 169 | *.nuget.props 170 | *.nuget.targets 171 | 172 | # Microsoft Azure Build Output 173 | csx/ 174 | *.build.csdef 175 | 176 | # Microsoft Azure Emulator 177 | ecf/ 178 | rcf/ 179 | 180 | # Windows Store app package directories and files 181 | AppPackages/ 182 | BundleArtifacts/ 183 | Package.StoreAssociation.xml 184 | _pkginfo.txt 185 | 186 | # Visual Studio cache files 187 | # files ending in .cache can be ignored 188 | *.[Cc]ache 189 | # but keep track of directories ending in .cache 190 | !*.[Cc]ache/ 191 | 192 | # Others 193 | ClientBin/ 194 | ~$* 195 | *~ 196 | *.dbmdl 197 | *.dbproj.schemaview 198 | *.jfm 199 | *.pfx 200 | *.publishsettings 201 | node_modules/ 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | 223 | # Business Intelligence projects 224 | *.rdl.data 225 | *.bim.layout 226 | *.bim_*.settings 227 | 228 | # Microsoft Fakes 229 | FakesAssemblies/ 230 | 231 | # GhostDoc plugin setting file 232 | *.GhostDoc.xml 233 | 234 | # Node.js Tools for Visual Studio 235 | .ntvs_analysis.dat 236 | 237 | # Visual Studio 6 build log 238 | *.plg 239 | 240 | # Visual Studio 6 workspace options file 241 | *.opt 242 | 243 | # Visual Studio LightSwitch build output 244 | **/*.HTMLClient/GeneratedArtifacts 245 | **/*.DesktopClient/GeneratedArtifacts 246 | **/*.DesktopClient/ModelManifest.xml 247 | **/*.Server/GeneratedArtifacts 248 | **/*.Server/ModelManifest.xml 249 | _Pvt_Extensions 250 | 251 | # Paket dependency manager 252 | .paket/paket.exe 253 | paket-files/ 254 | 255 | # FAKE - F# Make 256 | .fake/ 257 | 258 | # JetBrains Rider 259 | .idea/ 260 | *.sln.iml 261 | 262 | # CodeRush 263 | .cr/ 264 | 265 | # Python Tools for Visual Studio (PTVS) 266 | __pycache__/ 267 | *.pyc -------------------------------------------------------------------------------- /Binding/Secret/KeyVaultSecretConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Azure.KeyVault; 6 | using Microsoft.Azure.Services.AppAuthentication; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Description; 9 | using Microsoft.Azure.WebJobs.Host.Config; 10 | 11 | namespace Functions.Extensions.KeyVault 12 | { 13 | /// 14 | /// 15 | [Extension(@"KeyVaultSecret")] 16 | public class KeyVaultSecretConfiguration : IExtensionConfigProvider 17 | { 18 | // Make these static, particularly the HttpClient, so as not to exhaust the connection pool when using input & output bindings 19 | private static readonly HttpClient _httpClient = new HttpClient(); 20 | private static readonly AzureServiceTokenProvider _tokenProvider = new AzureServiceTokenProvider(); 21 | 22 | private static readonly KeyVaultClient _kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(_tokenProvider.KeyVaultTokenCallback), _httpClient); 23 | 24 | /// 25 | /// Initializes the specified context. 26 | /// 27 | /// The context. 28 | public void Initialize(ExtensionConfigContext context) 29 | { 30 | // Tell the Functions host that we want to add a new binding based on the KeyVaultSecret attribute class 31 | context.AddBindingRule() 32 | // Let funchost know it's an Input binding, and how to convert it to the type the user specifies (eg: string). If you want the user to be able to use other types, you must add more 'BindToInput' calls that return those types as values. Here, I have to use a class implementing the IAsyncConverter because I need to call async methods to perform the conversion 33 | .BindToInput(KeyVaultSecretInputConverter.Instance) 34 | // Add a validator on the user's attribute implementation to make sure I can even do the conversion and blow up accordingly if I can't. 35 | .AddValidator((attrib, t) => 36 | { 37 | if (string.IsNullOrWhiteSpace(attrib.ResourceNameSetting)) 38 | { 39 | throw new ArgumentException(nameof(attrib.ResourceNameSetting)); 40 | } 41 | 42 | if (string.IsNullOrWhiteSpace(attrib.SecretIdSetting)) 43 | { 44 | throw new ArgumentNullException(nameof(attrib.SecretIdSetting)); 45 | } 46 | }); 47 | 48 | context.AddBindingRule() 49 | .BindToCollector(KeyVaultSecretOutputConverter.Instance); 50 | } 51 | 52 | class KeyVaultSecretInputConverter : IAsyncConverter 53 | { 54 | private KeyVaultSecretInputConverter() { } 55 | 56 | // Provide a static instance to the keyvault converter so the funchost doesn't have to spin it up over and over, potentially exhausting connections or getting rate-limited 57 | public static KeyVaultSecretInputConverter Instance { get; } = new KeyVaultSecretInputConverter(); 58 | 59 | // "convert" means "take the attribute, and give me back the (in this case string) the user's asking for." So here, it means "go hit the keyvault instance they've specified and get the value for the secret" 60 | public async Task ConvertAsync(KeyVaultSecretAttribute attrib, CancellationToken cancellationToken) 61 | { 62 | var secretBundle = await _kvClient.GetSecretAsync($@"https://{attrib.ResourceNameSetting}.vault.azure.net/secrets/{attrib.SecretIdSetting}"); 63 | return secretBundle.Value; 64 | } 65 | } 66 | 67 | // Fortunately (or perhaps unfortunately in the case of this bilnding) every output binding boils down to an IAsyncCollector. This means that ultimately every output binding could be use to "collect" *multiple* changes. While this doesn't make sense for our KV Secret output binding, simply using it as an 'out string mySecret' in the Function eventually pipes it down in to this code where we set the value of the secret reference in the attribute. 68 | class KeyVaultSecretOutputConverter : IAsyncConverter> 69 | { 70 | private KeyVaultSecretOutputConverter() { } 71 | 72 | // Provide a static instance to the keyvault converter so the funchost doesn't have to spin it up over and over, potentially exhausting connections or getting rate-limited 73 | public static KeyVaultSecretOutputConverter Instance { get; } = new KeyVaultSecretOutputConverter(); 74 | 75 | public Task> ConvertAsync(KeyVaultSecretAttribute input, CancellationToken cancellationToken) 76 | { 77 | return Task.FromResult(new KeyVaultCollector(input) as IAsyncCollector); 78 | } 79 | 80 | private class KeyVaultCollector : IAsyncCollector 81 | { 82 | private readonly KeyVaultSecretAttribute _attrib; 83 | 84 | private Task _currentTask; 85 | 86 | public KeyVaultCollector(KeyVaultSecretAttribute attrib) 87 | { 88 | _attrib = attrib; 89 | } 90 | 91 | public Task AddAsync(string item, CancellationToken cancellationToken = default(CancellationToken)) 92 | { 93 | _currentTask = _kvClient.SetSecretAsync($@"https://{_attrib.ResourceNameSetting}.vault.azure.net", _attrib.SecretIdSetting, item); 94 | return _currentTask; 95 | } 96 | 97 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) 98 | { 99 | await _currentTask; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------