Skip to content

[Azure.Core] Phase 2a: AzureCredentialResolver + scope flip + ApiKey deletion#59256

Open
m-nash wants to merge 3 commits into
mainfrom
feature/credential-resolver-azurecore-phase2
Open

[Azure.Core] Phase 2a: AzureCredentialResolver + scope flip + ApiKey deletion#59256
m-nash wants to merge 3 commits into
mainfrom
feature/credential-resolver-azurecore-phase2

Conversation

@m-nash
Copy link
Copy Markdown
Member

@m-nash m-nash commented May 14, 2026

Summary

Phase 2a of the Azure.Core CredentialResolver migration. Introduces Azure.Identity.AzureCredentialResolver (experimental, SCME0002) — an Azure-flavored implementation of System.ClientModel.Primitives.CredentialResolver — plus convenience extensions on IConfiguration, IServiceCollection, and IHostApplicationBuilder. Also flips the Azure OpenAI default-scope quirk to write at the canonical SCM 1.12.0+ location, and stops the resolver from claiming ApiKeyCredential sections.

What's changed

New public API (SCME0002)

  • Azure.Identity.AzureCredentialResolver — a CredentialResolver that resolves Azure token credential sections (AzureCliCredential, ManagedIdentityCredential, ChainedTokenCredential, etc.) into TokenCredential instances.
  • IConfiguration.GetAzureCredential(sectionName, ...) — returns TokenCredential?; null when no resolver claims the section. Never throws.
  • IConfiguration.GetAzureClientSettings<T>(sectionName, params CredentialResolver[] resolvers) plus IEnumerable<CredentialResolver> + Action<IConfigurationSection> overloads.
  • IServiceCollection.AddAzureCredentialResolver() and IHostApplicationBuilder.AddAzureCredentialResolver() — idempotent DI registration.

ApiKeyCredential ownership

AzureCredentialResolver does not claim ApiKeyCredential sections. Service-specific Azure clients dispatch on Credential.CredentialSource themselves and read Credential.Key directly. ApiKeyTokenCredential (internal) was deleted.

Azure OpenAI default-scope flip

The default-scope quirk now writes Credential:Scope at the credential-section root (canonical SCM 1.12.0+ location) instead of Credential:AdditionalProperties:Scope. SCM 1.12.0's AuthenticationPolicy.Create reads from both locations (root preferred, AdditionalProperties as fallback) so existing configurations continue to work.

Tests

  • 35 AzureCredentialResolver resolver tests (all token sources, chained, missing sections).
  • 6 Azure OpenAI default-scope tests covering both the resolver-aware path and the legacy path × {OpenAI endpoint writes Scope, non-OpenAI doesn't, pre-set Scope is preserved}.
  • DI/standalone coverage for GetAzureCredential and GetAzureClientSettings<T>.
  • Compile-validated doc snippets backing ConfigurationAndDependencyInjection.md and ExperimentalFeatures.md.

Build / test status

  • All 5 TFMs build clean (net462, net472, netstandard2.0, net8.0, net10.0).
  • ApiCompat clean.
  • 35/35 CredentialResolvers tests pass on net8.0.
  • Broader Identity sweep: 4784 passed; the 2 pre-existing BrokerCredentialTests.GetToken_ExceptionType_MatchesExpected failures are unrelated to this PR.

Notes

  • This is phase 2a of a multi-phase migration. WithAzureCredential and ConfigurableCredential are unchanged in this PR and will be removed in phase 2b.
  • API surface diff: +14 additive lines, no removals (ApiKeyTokenCredential was internal).

…deletion

- Add experimental AzureCredentialResolver (SCME0002) that resolves Azure
  token credential sections via System.ClientModel.Primitives.CredentialResolver.
  ApiKeyCredential sections are intentionally not claimed; clients dispatch on
  Credential.CredentialSource themselves.
- Add IConfiguration.GetAzureCredential, IConfiguration.GetAzureClientSettings<T>
  resolver-aware overloads, and AddAzureCredentialResolver on IServiceCollection
  and IHostApplicationBuilder.
- Flip Azure OpenAI default-scope quirk to write Credential:Scope at the credential
  section root (canonical SCM 1.12.0+ location). SCM 1.12.0 reads both locations
  so existing configs continue to work.
- Tests: 35 resolver tests + 6 scope-flip tests + DI/standalone coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 17:38
@m-nash m-nash requested review from a team, JonathanCrd and christothes as code owners May 14, 2026 17:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Phase 2a of the Azure.Core CredentialResolver migration. Introduces a new experimental (SCME0002) Azure.Identity.AzureCredentialResolver plus convenience extensions on IConfiguration, IServiceCollection, and IHostApplicationBuilder for resolving Azure token credentials from configuration. Also flips the Azure OpenAI default-scope quirk to write Credential:Scope at the canonical SCM 1.12.0+ location, and stops claiming ApiKeyCredential sections so consuming clients dispatch on Credential.CredentialSource themselves.

Changes:

  • New public type AzureCredentialResolver and extension methods GetAzureCredential, resolver-aware GetAzureClientSettings<T>, and AddAzureCredentialResolver on DI/host builders.
  • Azure OpenAI default-scope is now written at the credential-section root instead of AdditionalProperties:Scope (legacy WithAzureCredential path updated to match).
  • New tests (resolver behavior, DI idempotency, default-scope quirk on both code paths) and compile-validated doc snippets backing updated ConfigurationAndDependencyInjection.md and ExperimentalFeatures.md.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.

Show a summary per file
File Description
sdk/core/Azure.Core/src/Identity/AzureCredentialResolver.cs New resolver with ApiKey opt-out and Chained/single-source dispatch.
sdk/core/Azure.Core/src/Identity/ConfigurationExtensions.cs New GetAzureCredential, resolver-aware GetAzureClientSettings<T>, AddAzureCredentialResolver; root-scope flip.
sdk/core/Azure.Core/api/Azure.Core.{net462,net472,netstandard2.0,net8.0,net10.0}.cs Public API baselines updated additively.
sdk/core/Azure.Core/src/docs/ConfigurationAndDependencyInjection.md Reworked sections + new Custom Credential Resolvers guidance.
sdk/core/Azure.Core/src/docs/ExperimentalFeatures.md Updated extension list and equivalence snippet.
sdk/core/Azure.Core/CHANGELOG.md Adds entries describing the new APIs and scope-flip.
sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureCredentialResolverTests.cs Resolver dispatch coverage including ApiKey opt-out and chained path.
sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureCredentialResolverDITests.cs DI registration idempotency.
sdk/core/Azure.Core/tests/Identity/CredentialResolvers/GetAzureCredentialTests.cs Helper coverage including custom-vs-built-in precedence and overrides.
sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureOpenAIDefaultScopeTests.cs Default-scope quirk on resolver-aware and legacy paths.
sdk/core/Azure.Core/tests/Identity/samples/DocSnippets/*.cs Compile-validated doc snippet types and snippet methods.

params CredentialResolver[] resolvers)
where T : ClientSettings, new()
{
CredentialResolver[] combined = [.. resolvers ?? [], AzureCredentialResolver.Instance];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a nifty little splat pattern.

using IHost host = builder.Build();
CredentialResolver[] resolvers = host.Services.GetServices<CredentialResolver>().ToArray();

Assert.AreEqual(1, resolvers.Length);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move to Assert.That format; it's required by NUnit 4+ and we should try to avoid accumulating more migration debt since we're trying to move the repo.

{
IConfiguration config = BuildConfig(new Dictionary<string, string>());

Assert.IsNull(config.GetAzureCredential("Nope"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m-nash and others added 2 commits May 19, 2026 13:15
Modern AddAzureClient / AddKeyedAzureClient / GetAzureClientSettings no
longer route through WithAzureCredential (which transitively uses
ClientSettings.PostConfigure, IClientBuilder.PostConfigure, and the
ClientSettings.CredentialProvider shim — all slated for SCM phase 5
removal). They register AzureCredentialResolver.Instance via
TryAddEnumerable and apply the Azure OpenAI default-scope quirk by
writing the scope directly to the original IConfiguration credential
section (when it exists), matching the WithAzureCredential side-effect
contract.

AddAzureCredentialResolver now registers the static singleton instance
instead of activating a new one so DI and standalone paths share the
same SCM CredentialCache bucket (cross-path credential instance sharing
preserved with the new API surface).

Adopt the SCM 1.13.0 surface: rename GetAzureCredential ->
GetAzureCredentialSettings (returns CredentialSettings? so ApiKey
callers can dispatch on Credential.Key). API baselines regenerated for
all 5 TFMs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
```C# Snippet:Azure_Core_Samples_AzureClient_GetCredential
TokenCredential credential = configuration.GetAzureCredential("MyClient:Credential");
```C# Snippet:Azure_Core_Samples_AzureClient_GetCredentialSettings
CredentialSettings credential = configuration.GetAzureCredentialSettings("MyClient:Credential");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't there also a case where a true token provider would be returned and you'd have to invoke the factory to create the credential?

// the same section. Skips when the configured endpoint is not an
// AzureOpenAI endpoint or when the Credential section is absent — we
// never materialize a Credential section just to attach a scope.
private static void ApplyAzureOpenAIDefaultScopeIfNeeded(IConfiguration configuration, string sectionName)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is suuuuper specific. What's the story?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants