diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml
index 4808ac18433d..4a18a0522ef7 100644
--- a/eng/.docsettings.yml
+++ b/eng/.docsettings.yml
@@ -70,6 +70,7 @@ known_presence_issues:
- ['sdk/keyvault','#5499']
- ['sdk/eventhub','#5499']
- ['sdk/attestation/Microsoft.Azure.Attestation','#5499']
+ - ['sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration', '#9939']
- ['sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection','#9955']
# List for changelogs begins here
diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props
index eb8e12dbb39d..1f9173bb6d2e 100755
--- a/eng/Packages.Data.props
+++ b/eng/Packages.Data.props
@@ -119,6 +119,8 @@
+
+
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/api/Azure.Security.KeyVault.Secrets.Extensions.Configuration.netstandard2.0.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/api/Azure.Security.KeyVault.Secrets.Extensions.Configuration.netstandard2.0.cs
new file mode 100644
index 000000000000..288e5471b3c2
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/api/Azure.Security.KeyVault.Secrets.Extensions.Configuration.netstandard2.0.cs
@@ -0,0 +1,34 @@
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ public partial class AzureKeyVaultConfigurationOptions
+ {
+ public AzureKeyVaultConfigurationOptions() { }
+ public AzureKeyVaultConfigurationOptions(System.Uri vaultUri, Azure.Core.TokenCredential credential) { }
+ public Azure.Security.KeyVault.Secrets.SecretClient Client { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public Azure.Security.KeyVault.Secrets.Extensions.Configuration.IKeyVaultSecretManager Manager { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public System.TimeSpan? ReloadInterval { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ }
+ public partial class DefaultKeyVaultSecretManager : Azure.Security.KeyVault.Secrets.Extensions.Configuration.IKeyVaultSecretManager
+ {
+ public DefaultKeyVaultSecretManager() { }
+ public virtual string GetKey(Azure.Security.KeyVault.Secrets.KeyVaultSecret secret) { throw null; }
+ public virtual bool Load(Azure.Security.KeyVault.Secrets.SecretProperties secret) { throw null; }
+ }
+ public partial interface IKeyVaultSecretManager
+ {
+ string GetKey(Azure.Security.KeyVault.Secrets.KeyVaultSecret secret);
+ bool Load(Azure.Security.KeyVault.Secrets.SecretProperties secret);
+ }
+}
+namespace Microsoft.Extensions.Configuration
+{
+ public static partial class AzureKeyVaultConfigurationExtensions
+ {
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, Azure.Security.KeyVault.Secrets.Extensions.Configuration.AzureKeyVaultConfigurationOptions options) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, Azure.Security.KeyVault.Secrets.SecretClient client, Azure.Security.KeyVault.Secrets.Extensions.Configuration.IKeyVaultSecretManager manager) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Uri vaultUri) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Uri vaultUri, Azure.Core.TokenCredential credential) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Uri vaultUri, Azure.Core.TokenCredential credential, Azure.Security.KeyVault.Secrets.Extensions.Configuration.IKeyVaultSecretManager manager) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddAzureKeyVault(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Uri vaultUri, Azure.Security.KeyVault.Secrets.Extensions.Configuration.IKeyVaultSecretManager manager) { throw null; }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj
new file mode 100644
index 000000000000..0f741f5a3862
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Azure Key Vault configuration provider implementation for Microsoft.Extensions.Configuration.
+ $(RequiredTargetFrameworks)
+ $(PackageTags);azure;keyvault
+ 1.0.0-preview.1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationExtensions.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationExtensions.cs
new file mode 100644
index 000000000000..b25c444f20d5
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationExtensions.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core;
+using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+using Azure.Security.KeyVault.Secrets.Extensions.Configuration;
+
+#pragma warning disable AZC0001 // Extension methods have to be in the correct namespace to appear in intellisense.
+namespace Microsoft.Extensions.Configuration
+#pragma warning restore
+{
+ ///
+ /// Extension methods for registering with .
+ ///
+ public static class AzureKeyVaultConfigurationExtensions
+ {
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// The Azure Key Vault uri.
+ /// The credential to to use for authentication.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(
+ this IConfigurationBuilder configurationBuilder,
+ Uri vaultUri,
+ TokenCredential credential)
+ {
+ return AddAzureKeyVault(configurationBuilder, vaultUri, credential, new DefaultKeyVaultSecretManager());
+ }
+
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// Azure Key Vault uri.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(
+ this IConfigurationBuilder configurationBuilder,
+ Uri vaultUri)
+ {
+ return AddAzureKeyVault(configurationBuilder, vaultUri, new DefaultAzureCredential());
+ }
+
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// Azure Key Vault uri.
+ /// The instance used to control secret loading.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(
+ this IConfigurationBuilder configurationBuilder,
+ Uri vaultUri,
+ IKeyVaultSecretManager manager)
+ {
+ return AddAzureKeyVault(configurationBuilder, vaultUri, new DefaultAzureCredential(), manager);
+ }
+
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// Azure Key Vault uri.
+ /// The credential to to use for authentication.
+ /// The instance used to control secret loading.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(
+ this IConfigurationBuilder configurationBuilder,
+ Uri vaultUri,
+ TokenCredential credential,
+ IKeyVaultSecretManager manager)
+ {
+ return AddAzureKeyVault(configurationBuilder, new AzureKeyVaultConfigurationOptions(vaultUri, credential)
+ {
+ Manager = manager
+ });
+ }
+
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// The to use for retrieving values.
+ /// The instance used to control secret loading.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(
+ this IConfigurationBuilder configurationBuilder,
+ SecretClient client,
+ IKeyVaultSecretManager manager)
+ {
+ return configurationBuilder.Add(new AzureKeyVaultConfigurationSource(new AzureKeyVaultConfigurationOptions()
+ {
+ Client = client,
+ Manager = manager
+ }));
+ }
+
+ ///
+ /// Adds an that reads configuration values from the Azure KeyVault.
+ ///
+ /// The to add to.
+ /// The to use.
+ /// The .
+ public static IConfigurationBuilder AddAzureKeyVault(this IConfigurationBuilder configurationBuilder, AzureKeyVaultConfigurationOptions options)
+ {
+ Argument.AssertNotNull(configurationBuilder, nameof(configurationBuilder));
+ Argument.AssertNotNull(options, nameof(configurationBuilder));
+ Argument.AssertNotNull(options.Client, $"{nameof(options)}.{nameof(options.Client)}");
+ Argument.AssertNotNull(options.Manager, $"{nameof(options)}.{nameof(options.Manager)}");
+
+ configurationBuilder.Add(new AzureKeyVaultConfigurationSource(options));
+
+ return configurationBuilder;
+ }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationOptions.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationOptions.cs
new file mode 100644
index 000000000000..60c09b47d16b
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationOptions.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core;
+using Microsoft.Extensions.Configuration;
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ ///
+ /// Options class used by the .
+ ///
+ public class AzureKeyVaultConfigurationOptions
+ {
+ ///
+ /// Creates a new instance of .
+ ///
+ public AzureKeyVaultConfigurationOptions()
+ {
+ Manager = DefaultKeyVaultSecretManager.Instance;
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// Azure Key Vault uri.
+ /// The to use for authentication.
+ public AzureKeyVaultConfigurationOptions(
+ Uri vaultUri,
+ TokenCredential credential) : this()
+ {
+ Client = new SecretClient(vaultUri, credential);
+ }
+
+ ///
+ /// Gets or sets the to use for retrieving values.
+ ///
+ public SecretClient Client { get; set; }
+
+ ///
+ /// Gets or sets the instance used to control secret loading.
+ ///
+ public IKeyVaultSecretManager Manager { get; set; }
+
+ ///
+ /// Gets or sets the timespan to wait between attempts at polling the Azure Key Vault for changes. null to disable reloading.
+ ///
+ public TimeSpan? ReloadInterval { get; set; }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationProvider.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationProvider.cs
new file mode 100644
index 000000000000..3dd0b3d7731e
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationProvider.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core;
+using Microsoft.Extensions.Configuration;
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ ///
+ /// An AzureKeyVault based .
+ ///
+ internal class AzureKeyVaultConfigurationProvider : ConfigurationProvider, IDisposable
+ {
+ private readonly TimeSpan? _reloadInterval;
+ private readonly SecretClient _client;
+ private readonly IKeyVaultSecretManager _manager;
+ private Dictionary _loadedSecrets;
+ private Task _pollingTask;
+ private readonly CancellationTokenSource _cancellationToken;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The to use for retrieving values.
+ /// The to use in managing values.
+ /// The timespan to wait in between each attempt at polling the Azure Key Vault for changes. Default is null which indicates no reloading.
+ public AzureKeyVaultConfigurationProvider(SecretClient client, IKeyVaultSecretManager manager, TimeSpan? reloadInterval = null)
+ {
+ Argument.AssertNotNull(client, nameof(client));
+ Argument.AssertNotNull(manager, nameof(manager));
+
+ _client = client;
+ _manager = manager;
+ if (reloadInterval != null && reloadInterval.Value <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(reloadInterval), reloadInterval, nameof(reloadInterval) + " must be positive.");
+ }
+
+ _pollingTask = null;
+ _cancellationToken = new CancellationTokenSource();
+ _reloadInterval = reloadInterval;
+ }
+
+ ///
+ /// Load secrets into this provider.
+ ///
+ public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+
+ private async Task PollForSecretChangesAsync()
+ {
+ while (!_cancellationToken.IsCancellationRequested)
+ {
+ await WaitForReload().ConfigureAwait(false);
+ try
+ {
+ await LoadAsync().ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ // Ignore
+ }
+ }
+ }
+
+ protected virtual Task WaitForReload()
+ {
+ // WaitForReload is only called when the _reloadInterval has a value.
+ return Task.Delay(_reloadInterval.Value, _cancellationToken.Token);
+ }
+
+ private async Task LoadAsync()
+ {
+ var secretPages = _client.GetPropertiesOfSecretsAsync();
+
+ var tasks = new List>>();
+ var newLoadedSecrets = new Dictionary();
+ var oldLoadedSecrets = Interlocked.Exchange(ref _loadedSecrets, null);
+
+ await foreach (var secret in secretPages.ConfigureAwait(false))
+ {
+ if (!_manager.Load(secret) || secret.Enabled != true)
+ {
+ continue;
+ }
+
+ var secretId = secret.Name;
+ if (oldLoadedSecrets != null &&
+ oldLoadedSecrets.TryGetValue(secretId, out var existingSecret) &&
+ existingSecret.IsUpToDate(secret.UpdatedOn))
+ {
+ oldLoadedSecrets.Remove(secretId);
+ newLoadedSecrets.Add(secretId, existingSecret);
+ }
+ else
+ {
+ tasks.Add(_client.GetSecretAsync(secret.Name));
+ }
+ }
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ foreach (var task in tasks)
+ {
+ var secretBundle = task.Result;
+ newLoadedSecrets.Add(secretBundle.Value.Name, new LoadedSecret(_manager.GetKey(secretBundle), secretBundle.Value.Value, secretBundle.Value.Properties.UpdatedOn));
+ }
+
+ _loadedSecrets = newLoadedSecrets;
+
+ // Reload is needed if we are loading secrets that were not loaded before or
+ // secret that was loaded previously is not available anymore
+ if (tasks.Any() || oldLoadedSecrets?.Any() == true)
+ {
+ SetData(_loadedSecrets, fireToken: oldLoadedSecrets != null);
+ }
+
+ // schedule a polling task only if none exists and a valid delay is specified
+ if (_pollingTask == null && _reloadInterval != null)
+ {
+ _pollingTask = PollForSecretChangesAsync();
+ }
+ }
+
+ private void SetData(Dictionary loadedSecrets, bool fireToken)
+ {
+ var data = new Dictionary(loadedSecrets.Count, StringComparer.OrdinalIgnoreCase);
+ foreach (var secretItem in loadedSecrets)
+ {
+ data.Add(secretItem.Value.Key, secretItem.Value.Value);
+ }
+
+ Data = data;
+ if (fireToken)
+ {
+ OnReload();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _cancellationToken.Cancel();
+ }
+
+ private readonly struct LoadedSecret
+ {
+ public LoadedSecret(string key, string value, DateTimeOffset? updated)
+ {
+ Key = key;
+ Value = value;
+ Updated = updated;
+ }
+
+ public string Key { get; }
+ public string Value { get; }
+ public DateTimeOffset? Updated { get; }
+
+ public bool IsUpToDate(DateTimeOffset? updated)
+ {
+ if (updated.HasValue != Updated.HasValue)
+ {
+ return false;
+ }
+
+ return updated.GetValueOrDefault() == Updated.GetValueOrDefault();
+ }
+ }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationSource.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationSource.cs
new file mode 100644
index 000000000000..c28f1a43b26c
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/AzureKeyVaultConfigurationSource.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.Configuration;
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ ///
+ /// Represents Azure Key Vault secrets as an .
+ ///
+ internal class AzureKeyVaultConfigurationSource : IConfigurationSource
+ {
+ private readonly AzureKeyVaultConfigurationOptions _options;
+
+ public AzureKeyVaultConfigurationSource(AzureKeyVaultConfigurationOptions options)
+ {
+ _options = options;
+ }
+
+ ///
+ public IConfigurationProvider Build(IConfigurationBuilder builder)
+ {
+ return new AzureKeyVaultConfigurationProvider(_options.Client, _options.Manager, _options.ReloadInterval);
+ }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/DefaultKeyVaultSecretManager.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/DefaultKeyVaultSecretManager.cs
new file mode 100644
index 000000000000..e617de01c489
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/DefaultKeyVaultSecretManager.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.Configuration;
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ ///
+ /// Default implementation of that loads all secrets
+ /// and replaces '--' with ':" in key names.
+ ///
+ public class DefaultKeyVaultSecretManager : IKeyVaultSecretManager
+ {
+ internal static IKeyVaultSecretManager Instance { get; } = new DefaultKeyVaultSecretManager();
+
+ ///
+ public virtual string GetKey(KeyVaultSecret secret)
+ {
+ return secret.Name.Replace("--", ConfigurationPath.KeyDelimiter);
+ }
+
+ ///
+ public virtual bool Load(SecretProperties secret)
+ {
+ return true;
+ }
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/IKeyVaultSecretManager.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/IKeyVaultSecretManager.cs
new file mode 100644
index 000000000000..9b716723375d
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/IKeyVaultSecretManager.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration
+{
+ ///
+ /// The instance used to control secret loading.
+ ///
+ public interface IKeyVaultSecretManager
+ {
+ ///
+ /// Checks if value should be retrieved.
+ ///
+ /// The instance.
+ /// true if secrets value should be loaded, otherwise false.
+ bool Load(SecretProperties secret);
+
+ ///
+ /// Maps secret to a configuration key.
+ ///
+ /// The instance.
+ /// Configuration key name to store secret value.
+ string GetKey(KeyVaultSecret secret);
+ }
+}
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Properties/AssemblyInfo.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..e17e21839715
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
+[assembly: InternalsVisibleTo("Azure.Security.KeyVault.Secrets.Extensions.Configuration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")]
\ No newline at end of file
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/Azure.Security.KeyVault.Secrets.Extensions.Configuration.Tests.csproj b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/Azure.Security.KeyVault.Secrets.Extensions.Configuration.Tests.csproj
new file mode 100644
index 000000000000..4d3eb067eca6
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/Azure.Security.KeyVault.Secrets.Extensions.Configuration.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ $(RequiredTargetFrameworks)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/AzureKeyVaultConfigurationTest.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/AzureKeyVaultConfigurationTest.cs
new file mode 100644
index 000000000000..daf151caad8d
--- /dev/null
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/test/AzureKeyVaultConfigurationTest.cs
@@ -0,0 +1,529 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core.Testing;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using NUnit.Framework;
+
+namespace Azure.Security.KeyVault.Secrets.Extensions.Configuration.Tests
+{
+ public class AzureKeyVaultConfigurationTest
+ {
+ private static readonly TimeSpan NoReloadDelay = TimeSpan.FromMilliseconds(1);
+
+ private void SetPages(Mock mock, params KeyVaultSecret[][] pages)
+ {
+ SetPages(mock, null, pages);
+ }
+
+ private void SetPages(Mock mock, Func getSecretCallback, params KeyVaultSecret[][] pages)
+ {
+ getSecretCallback ??= (_ => Task.CompletedTask);
+
+ var pagesOfProperties = pages.Select(
+ page => page.Select(secret => secret.Properties).ToArray()).ToArray();
+
+ mock.Setup(m => m.GetPropertiesOfSecretsAsync(default)).Returns(new MockAsyncPageable(pagesOfProperties));
+
+ foreach (var page in pages)
+ {
+ foreach (var secret in page)
+ {
+ mock.Setup(client => client.GetSecretAsync(secret.Name, null, default))
+ .Callback((string name, string label, CancellationToken token) => getSecretCallback(name))
+ .ReturnsAsync(Response.FromValue(secret, Mock.Of()));
+ }
+ }
+ }
+
+ private class MockAsyncPageable : AsyncPageable
+ {
+ private readonly SecretProperties[][] _pages;
+
+ public MockAsyncPageable(SecretProperties[][] pages)
+ {
+ _pages = pages;
+ }
+
+ public override async IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null)
+ {
+ foreach (var page in _pages)
+ {
+ yield return Page.FromValues(page, null, Mock.Of());
+ }
+
+ await Task.CompletedTask;
+ }
+ }
+
+ [Test]
+ public void LoadsAllSecretsFromVault()
+ {
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1")
+ },
+ new[]
+ {
+ CreateSecret("Secret2", "Value2")
+ }
+ );
+
+ // Act
+ using (var provider = new AzureKeyVaultConfigurationProvider(client.Object, new DefaultKeyVaultSecretManager()))
+ {
+ provider.Load();
+
+ var childKeys = provider.GetChildKeys(Enumerable.Empty(), null).ToArray();
+ Assert.AreEqual(new[] { "Secret1", "Secret2" }, childKeys);
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+ Assert.AreEqual("Value2", provider.Get("Secret2"));
+ }
+ }
+
+ private KeyVaultSecret CreateSecret(string name, string value, bool? enabled = true, DateTimeOffset? updated = null)
+ {
+ var id = new Uri("http://azure.keyvault/" + name);
+
+ var secretProperties = SecretModelFactory.SecretProperties(id, name: name, updatedOn: updated);
+ secretProperties.Enabled = enabled;
+
+ return SecretModelFactory.KeyVaultSecret(secretProperties, value);
+ }
+
+ [Test]
+ public void DoesNotLoadFilteredItems()
+ {
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1")
+ },
+ new[]
+ {
+ CreateSecret("Secret2", "Value2")
+ }
+ );
+
+ // Act
+ using (var provider = new AzureKeyVaultConfigurationProvider(client.Object, new EndsWithOneKeyVaultSecretManager()))
+ {
+ provider.Load();
+
+ // Assert
+ var childKeys = provider.GetChildKeys(Enumerable.Empty(), null).ToArray();
+ Assert.AreEqual(new[] { "Secret1" }, childKeys);
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+ }
+ }
+
+ [Test]
+ public void DoesNotLoadDisabledItems()
+ {
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1")
+ },
+ new[]
+ {
+ CreateSecret("Secret2", "Value2", enabled: false),
+ CreateSecret("Secret3", "Value3", enabled: null),
+ }
+ );
+
+ // Act
+ using (var provider = new AzureKeyVaultConfigurationProvider(client.Object, new DefaultKeyVaultSecretManager()))
+ {
+ provider.Load();
+
+ // Assert
+ var childKeys = provider.GetChildKeys(Enumerable.Empty(), null).ToArray();
+ Assert.AreEqual(new[] { "Secret1" }, childKeys);
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+ Assert.Throws(() => provider.Get("Secret2"));
+ Assert.Throws(() => provider.Get("Secret3"));
+ }
+ }
+
+ [Test]
+ public void SupportsReload()
+ {
+ var updated = DateTime.Now;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1", enabled: true, updated: updated)
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new AzureKeyVaultConfigurationProvider(client.Object, new DefaultKeyVaultSecretManager()))
+ {
+ provider.Load();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value2", enabled: true, updated: updated.AddSeconds(1))
+ }
+ );
+
+ provider.Load();
+ Assert.AreEqual("Value2", provider.Get("Secret1"));
+ }
+ }
+
+ [Test]
+ public async Task SupportsAutoReload()
+ {
+ var updated = DateTime.Now;
+ int numOfTokensFired = 0;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1", enabled: true, updated: updated)
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new ReloadControlKeyVaultProvider(client.Object, new DefaultKeyVaultSecretManager(), reloadPollDelay: NoReloadDelay))
+ {
+ ChangeToken.OnChange(
+ () => provider.GetReloadToken(),
+ () => {
+ numOfTokensFired++;
+ });
+
+ provider.Load();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+
+ await provider.Wait();
+
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value2", enabled: true, updated: updated.AddSeconds(1))
+ }
+ );
+
+ provider.Release();
+
+ await provider.Wait();
+
+ Assert.AreEqual("Value2", provider.Get("Secret1"));
+ Assert.AreEqual(1, numOfTokensFired);
+ }
+ }
+
+ [Test]
+ public async Task DoesntReloadUnchanged()
+ {
+ var updated = DateTime.Now;
+ int numOfTokensFired = 0;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1", enabled: true, updated: updated)
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new ReloadControlKeyVaultProvider(client.Object, new DefaultKeyVaultSecretManager(), reloadPollDelay: NoReloadDelay))
+ {
+ ChangeToken.OnChange(
+ () => provider.GetReloadToken(),
+ () => {
+ numOfTokensFired++;
+ });
+
+ provider.Load();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+
+ await provider.Wait();
+
+ provider.Release();
+
+ await provider.Wait();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+ Assert.AreEqual(0, numOfTokensFired);
+ }
+ }
+
+ [Test]
+ public async Task SupportsReloadOnRemove()
+ {
+ int numOfTokensFired = 0;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1"),
+ CreateSecret("Secret2", "Value2")
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new ReloadControlKeyVaultProvider(client.Object, new DefaultKeyVaultSecretManager(), reloadPollDelay: NoReloadDelay))
+ {
+ ChangeToken.OnChange(
+ () => provider.GetReloadToken(),
+ () => {
+ numOfTokensFired++;
+ });
+
+ provider.Load();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+
+ await provider.Wait();
+
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value2")
+ }
+ );
+
+ provider.Release();
+
+ await provider.Wait();
+
+ Assert.Throws(() => provider.Get("Secret2"));
+ Assert.AreEqual(1, numOfTokensFired);
+ }
+ }
+
+ [Test]
+ public async Task SupportsReloadOnEnabledChange()
+ {
+ int numOfTokensFired = 0;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1"),
+ CreateSecret("Secret2", "Value2")
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new ReloadControlKeyVaultProvider(client.Object, new DefaultKeyVaultSecretManager(), reloadPollDelay: NoReloadDelay))
+ {
+ ChangeToken.OnChange(
+ () => provider.GetReloadToken(),
+ () => {
+ numOfTokensFired++;
+ });
+
+ provider.Load();
+
+ Assert.AreEqual("Value2", provider.Get("Secret2"));
+
+ await provider.Wait();
+
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value2"),
+ CreateSecret("Secret2", "Value2", enabled: false)
+ }
+ );
+
+ provider.Release();
+
+ await provider.Wait();
+
+ Assert.Throws(() => provider.Get("Secret2"));
+ Assert.AreEqual(1, numOfTokensFired);
+ }
+ }
+
+ [Test]
+ public async Task SupportsReloadOnAdd()
+ {
+ int numOfTokensFired = 0;
+
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1")
+ }
+ );
+
+ // Act & Assert
+ using (var provider = new ReloadControlKeyVaultProvider(client.Object, new DefaultKeyVaultSecretManager(), reloadPollDelay: NoReloadDelay))
+ {
+ ChangeToken.OnChange(
+ () => provider.GetReloadToken(),
+ () => {
+ numOfTokensFired++;
+ });
+
+ provider.Load();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+
+ await provider.Wait();
+
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Secret1", "Value1"),
+ },
+ new[]
+ {
+ CreateSecret("Secret2", "Value2")
+ }
+ );
+
+ provider.Release();
+
+ await provider.Wait();
+
+ Assert.AreEqual("Value1", provider.Get("Secret1"));
+ Assert.AreEqual("Value2", provider.Get("Secret2"));
+ Assert.AreEqual(1, numOfTokensFired);
+ }
+ }
+
+ [Test]
+ public void ReplaceDoubleMinusInKeyName()
+ {
+ var client = new Mock();
+ SetPages(client,
+ new[]
+ {
+ CreateSecret("Section--Secret1", "Value1")
+ }
+ );
+
+ // Act
+ using (var provider = new AzureKeyVaultConfigurationProvider(client.Object, new DefaultKeyVaultSecretManager()))
+ {
+ provider.Load();
+
+ // Assert
+ Assert.AreEqual("Value1", provider.Get("Section:Secret1"));
+ }
+ }
+
+ [Test]
+ public async Task LoadsSecretsInParallel()
+ {
+ var tcs = new TaskCompletionSource