-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Support credential instance reuse in WithAzureCredential DI path #56245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
sdk/identity/Azure.Identity/src/ConfigurableCredentialCache.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Linq; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using Microsoft.Extensions.Configuration; | ||
|
|
||
| namespace Azure.Identity | ||
| { | ||
| [Experimental("SCME0002")] | ||
| internal static class ConfigurableCredentialCache | ||
| { | ||
| private static ConcurrentDictionary<string, ConfigurableCredential>? s_cache; | ||
| private static ConcurrentDictionary<string, ConfigurableCredential> Cache => | ||
| LazyInitializer.EnsureInitialized(ref s_cache, static () => new ConcurrentDictionary<string, ConfigurableCredential>())!; | ||
|
|
||
| public static ConfigurableCredential GetOrAdd(IConfigurationSection credentialSection, Func<ConfigurableCredential> factory) | ||
| { | ||
| string key = CreateKey(credentialSection); | ||
| return Cache.GetOrAdd(key, _ => factory()); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a deterministic cache key from the content of an <see cref="IConfigurationSection"/>. | ||
| /// Two sections at different paths but with identical values will produce the same key. | ||
| /// The key is a SHA256 hash to avoid leaking secrets that may be present in configuration values. | ||
| /// </summary> | ||
| private static string CreateKey(IConfigurationSection section) | ||
| { | ||
| string basePath = section.Path; | ||
| int prefixLength = basePath.Length > 0 ? basePath.Length + 1 : 0; // +1 for the ':' separator | ||
|
|
||
| IEnumerable<KeyValuePair<string, string?>> entries = section.AsEnumerable() | ||
| .Where(kvp => kvp.Value is not null) | ||
| .OrderBy(kvp => kvp.Key, StringComparer.Ordinal); | ||
|
|
||
| StringBuilder sb = new(); | ||
| foreach (KeyValuePair<string, string?> kvp in entries) | ||
| { | ||
| sb.Append(kvp.Key, prefixLength, kvp.Key.Length - prefixLength); | ||
| sb.Append('=').Append(kvp.Value).Append(';'); | ||
| } | ||
|
|
||
| byte[] inputBytes = Encoding.UTF8.GetBytes(sb.ToString()); | ||
| #if NETSTANDARD2_0 | ||
| using (SHA256 sha256 = SHA256.Create()) | ||
| { | ||
| byte[] hash = sha256.ComputeHash(inputBytes); | ||
| return BitConverter.ToString(hash).Replace("-", string.Empty); | ||
| } | ||
| #else | ||
| byte[] hash = SHA256.HashData(inputBytes); | ||
| return Convert.ToHexString(hash); | ||
| #endif | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
222 changes: 222 additions & 0 deletions
222
...identity/Azure.Identity/tests/ConfigurableCredentials/ConfigurableCredentialCacheTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.Configuration; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Azure.Identity.Tests.ConfigurableCredentials | ||
| { | ||
| public class ConfigurableCredentialCacheTests | ||
| { | ||
| private static IConfigurationSection BuildCredentialSection(string sectionPath, Dictionary<string, string> values) | ||
| { | ||
| var configData = new Dictionary<string, string>(); | ||
| foreach (var kvp in values) | ||
| { | ||
| configData[$"{sectionPath}:{kvp.Key}"] = kvp.Value; | ||
| } | ||
|
|
||
| var configuration = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(configData) | ||
| .Build(); | ||
|
|
||
| return configuration.GetSection(sectionPath); | ||
| } | ||
|
|
||
| // Each test uses a unique nonce in its values to avoid interference from the global static cache. | ||
| private static string Unique() => Guid.NewGuid().ToString("N"); | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_SameValuesDifferentPaths_ReturnsSameInstance() | ||
| { | ||
| string nonce = Unique(); | ||
| var values = new Dictionary<string, string> | ||
| { | ||
| ["TenantId"] = nonce, | ||
| ["CredentialSource"] = "AzureCli" | ||
| }; | ||
|
|
||
| var section1 = BuildCredentialSection("Client1:Credential", values); | ||
| var section2 = BuildCredentialSection("Client2:Credential", values); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_DifferentValues_ReturnsDifferentInstances() | ||
| { | ||
| var section1 = BuildCredentialSection("Client1:Credential", new Dictionary<string, string> | ||
| { | ||
| ["TenantId"] = Unique() | ||
| }); | ||
| var section2 = BuildCredentialSection("Client2:Credential", new Dictionary<string, string> | ||
| { | ||
| ["TenantId"] = Unique() | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreNotSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_EmptySections_ReturnsSameInstance() | ||
| { | ||
| var section1 = BuildCredentialSection("Client:Credential", new Dictionary<string, string>()); | ||
| var section2 = BuildCredentialSection("Other:Credential", new Dictionary<string, string>()); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_OrderIndependent_ReturnsSameInstance() | ||
| { | ||
| string nonce = Unique(); | ||
| var section1 = BuildCredentialSection("A:Credential", new Dictionary<string, string> | ||
| { | ||
| ["Zebra"] = nonce, | ||
| ["Alpha"] = "a" | ||
| }); | ||
| var section2 = BuildCredentialSection("B:Credential", new Dictionary<string, string> | ||
| { | ||
| ["Alpha"] = "a", | ||
| ["Zebra"] = nonce | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_SameValues_FactoryCalledOnce() | ||
| { | ||
| string nonce = Unique(); | ||
| int factoryCallCount = 0; | ||
|
|
||
| var section1 = BuildCredentialSection("Client1:Credential", new Dictionary<string, string> | ||
| { | ||
| ["TenantId"] = nonce | ||
| }); | ||
| var section2 = BuildCredentialSection("Client2:Credential", new Dictionary<string, string> | ||
| { | ||
| ["TenantId"] = nonce | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => | ||
| { | ||
| factoryCallCount++; | ||
| return new ConfigurableCredential(); | ||
| }); | ||
|
|
||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => | ||
| { | ||
| factoryCallCount++; | ||
| return new ConfigurableCredential(); | ||
| }); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| Assert.AreEqual(1, factoryCallCount); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_NestedValues_SameContentReturnsSameInstance() | ||
| { | ||
| string nonce = Unique(); | ||
| var configData1 = new Dictionary<string, string> | ||
| { | ||
| ["Client1:Credential:TenantId"] = nonce, | ||
| ["Client1:Credential:Nested:Value"] = "deep" | ||
| }; | ||
| var configData2 = new Dictionary<string, string> | ||
| { | ||
| ["Client2:Credential:TenantId"] = nonce, | ||
| ["Client2:Credential:Nested:Value"] = "deep" | ||
| }; | ||
|
|
||
| var config1 = new ConfigurationBuilder().AddInMemoryCollection(configData1).Build(); | ||
| var config2 = new ConfigurationBuilder().AddInMemoryCollection(configData2).Build(); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(config1.GetSection("Client1:Credential"), () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(config2.GetSection("Client2:Credential"), () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_SameArrayValues_ReturnsSameInstance() | ||
| { | ||
| string nonce = Unique(); | ||
| var section1 = BuildCredentialSection("Client1:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = "tenant-a", | ||
| ["AdditionallyAllowedTenants:1"] = "tenant-b", | ||
| }); | ||
| var section2 = BuildCredentialSection("Client2:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = "tenant-a", | ||
| ["AdditionallyAllowedTenants:1"] = "tenant-b", | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_DifferentArrayValues_ReturnsDifferentInstances() | ||
| { | ||
| string nonce = Unique(); | ||
| var section1 = BuildCredentialSection("Client1:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = Unique(), | ||
| }); | ||
| var section2 = BuildCredentialSection("Client2:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = Unique(), | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreNotSame(cred1, cred2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void GetOrAdd_DifferentArrayLength_ReturnsDifferentInstances() | ||
| { | ||
| string nonce = Unique(); | ||
| var section1 = BuildCredentialSection("Client1:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = "tenant-a", | ||
| }); | ||
| var section2 = BuildCredentialSection("Client2:Credential", new Dictionary<string, string> | ||
| { | ||
| ["CredentialSource"] = nonce, | ||
| ["AdditionallyAllowedTenants:0"] = "tenant-a", | ||
| ["AdditionallyAllowedTenants:1"] = "tenant-b", | ||
| }); | ||
|
|
||
| var cred1 = ConfigurableCredentialCache.GetOrAdd(section1, () => new ConfigurableCredential()); | ||
| var cred2 = ConfigurableCredentialCache.GetOrAdd(section2, () => new ConfigurableCredential()); | ||
|
|
||
| Assert.AreNotSame(cred1, cred2); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.