Resolve cloud-specific metadata by authority host - #3994
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness/DI-lifecycle and build-warning issues in the new resolver wiring (null/empty host handling, singleton bypass, and unused usings/variables) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR updates Microsoft.Identity.Web’s Federated Identity Credential (FIC) flows to automatically resolve the correct cloud-specific token-exchange audience/scope based on the authority host, layering (per-call override) > (injected ICloudMetadataProvider) > (MSAL baseline) > (public-cloud fallback), so sovereign/private clouds work without customer code in the common case.
Changes:
- Introduces an internal
CloudMetadataResolverand wires it into OIDC-CC FIC, agent identity FIC, and managed-identity FIC legs to avoid hardcoded public-cloud token-exchange values. - Adds a new public
AddCloudMetadata(IConfiguration)extension to register host-keyed cloud metadata from configuration. - Adds/updates unit and pseudo-E2E tests to validate cross-cloud resolution and override precedence.
File summaries
| File | Description |
|---|---|
| tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs | Updates/adds pseudo-E2E coverage to assert cloud-specific exchange scopes and override behavior. |
| tests/Microsoft.Identity.Web.Test/CloudMetadataResolverTests.cs | Adds unit tests covering resolver precedence, MSAL baseline resolution, config binding, and warning dedupe. |
| src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs | Uses resolver to compute agent FIC exchange scopes dynamically instead of a hardcoded public-cloud scope. |
| src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs | Registers CloudMetadataResolver as a singleton decision point via DI. |
| src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt | Declares new public API surface (AddCloudMetadata). |
| src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt | Declares new public API surface (AddCloudMetadata). |
| src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.cs | Resolves managed-identity FIC token-exchange audience from authority host when not explicitly configured. |
| src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataServiceCollectionExtensions.cs | Adds the new AddCloudMetadata(IConfiguration) configuration-driven provider registration. |
| src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs | Implements layered host-keyed resolution for token-exchange audience/scope, with fallback diagnostics. |
| src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs | Switches OIDC-CC FIC exchange to resolver-based scope computation (per-call override supported). |
| src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs | Passes a resolver into the provider so injected metadata providers can affect OIDC-CC FIC resolution. |
| src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs | Derives public-cloud default audience from MSAL cloud metadata (single source of truth) with fallback. |
| src/Microsoft.Identity.Web.Certificateless/CertificatelessConstants.cs | Adds a public-cloud host constant used for MSAL baseline lookups in managed-identity assertion flow. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 4
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Changes recommended
There are likely build-breaking issues (missing MSAL AppConfig using for TokenExchange* extensions and an unused test parameter that may be flagged by analyzers under warnings-as-errors).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs:407
scenariois currently unused inRunFicExchangeScenarioAsync. If code-style analyzers flag unused parameters during build, this can fail the build under TreatWarningsAsErrors. Either remove the parameter or explicitly consume it (e.g.,_ = scenario;).
private async Task RunFicExchangeScenarioAsync(
string scenario,
string? sourceInstance,
string? sourceAuthority,
string? customTokenExchangeUrl,
string expectedExchangeScope)
- Files reviewed: 13/13 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
5596c5a to
01f461a
Compare
Use IsNullOrEmpty(Authority) ? Instance : Authority in the agent-FIC legs so an empty-string authority falls through to Instance (mirrors the OIDC source-host fix); a plain ?? only guards null. Reword two comments to present-tense behavior descriptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f4470fd-e0ff-4d14-b2c7-ece13faf29ff
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
40496bd to
535d8e8
Compare
Makes Microsoft.Identity.Web resolve the correct cloud-specific Federated Identity Credential (FIC)
token-exchange audience/scope automatically from the request's authority host, instead of hardcoding the
public-cloud value. ID Web is the translator between MSAL's public cloud baseline and any caller- or
upstream-supplied metadata, so sovereign and private clouds work with no customer code in the common case,
while remaining fully overridable.
Related work in Abstractions and MSAL:
AzureAD/microsoft-identity-abstractions-for-dotnet#266
AzureAD/microsoft-authentication-library-for-dotnet#6104
Motivation
FIC-based flows must present a cloud-specific token-exchange audience (e.g.
api://AzureADTokenExchangeUSGovfor US Government) rather than the public-cloud
api://AzureADTokenExchange. ID Web previously hardcoded thepublic value across every FIC leg (agent identities, OIDC client-credentials, managed identity), so
applications in sovereign or private clouds hit opaque exchange failures with no override path.
This change centralizes the audience/scope decision in one place, keyed off the authority host, layering a
caller- or upstream-SDK metadata provider over MSAL's built-in baseline — so the right value is resolved
automatically for the clouds MSAL ships, and any cloud can be added or overridden.
Design
The resolver:
CloudMetadataResolution(internal, static)A single internal, static helper is the one place ID Web decides the FIC token-exchange value. Each FIC
leg holds an optional
ICloudMetadataProviderresolved from DI and passes it in per call. Resolution layers,highest precedence first:
TokenExchangeUrl);ICloudMetadataProviderfrom DI — contributed by a caller or an upstream SDK; this is howinternal-only sovereign clouds (which MSAL does not ship) become resolvable;
KnownCloudMetadata.Default.It exposes two forms so no call site hand-builds either:
ResolveTokenExchangeAudience(...)— the bare audience, for managed-identity / resource contexts.ResolveTokenExchangeScope(...)— the scope (bare +/.default), for client-credentials / app-tokencontexts. The
/.defaultcomputation is delegated to MSAL'sTokenExchangeScope.FromAudience, the singlecross-stack owner of that rule, so ID Web and MSAL can never diverge on the suffix.
Because the upstream provider and MSAL baseline expose the audience under the same key literal
(Abstractions'
CloudMetadataKeyNames.FederatedCredentialAudienceequals MSAL'sCloudMetadataKeyNames.FederatedCredentialAudience), ID Web "translates" between the two SDKs simply byreading the same literal from whichever source resolves first — no runtime key remapping is needed, and
neither SDK depends on the other.
Being static, the resolver needs no DI registration or lifetime management: each consumer resolves
ICloudMetadataProviderfrom the container (so the winningTryAddprovider is honored) and hands it in.Fail-fast on an unknown cloud
When a non-empty authority host resolves to no cloud-specific value from any source, ID Web throws an
InvalidOperationExceptionnaming the host and pointing at the override APIs (AddCloudMetadata(...), anICloudMetadataProviderregistration, or an explicitTokenExchangeUrl) — rather than silently exchangingagainst the wrong (public-cloud) audience. This "throw on an unknown cloud" behavior is deliberately chosen
over a fail-soft public-cloud fallback: switching to a fallback later would be a non-breaking relaxation,
whereas the reverse would break callers. The common public-cloud path is unaffected (it resolves from the
MSAL baseline), and the managed-identity leg — which has no AAD authority to key on — still uses the
documented public-cloud default rather than throwing.
Configuration binding:
AddCloudMetadata(IConfiguration)(new public API)For non-upstream callers that need a cloud ID Web and MSAL do not ship, a new public extension registers a
provider purely from configuration — no code beyond binding an
appsettings.jsonsection:It binds each host→key/value section into an Abstractions
InMemoryCloudMetadataProviderregistered asICloudMetadataProviderviaTryAddSingleton(so an explicitly registered provider — including an upstreamSDK's — wins and is left untouched). The section shape is identical to MISE's
AddMiseCloudMetadata(IConfiguration).Where resolution is applied
Every FIC leg that keys off an authority routes through the resolver:
OidcIdpSignedAssertionProvider) — resolves the exchange scope fromthe application's instance/authority host; an explicitly configured
TokenExchangeUrlis passed as theper-call override. The provider is injected via an internal constructor overload (from DI).
TokenAcquisition, blueprint leg 1 + instance leg 2) — resolves the exchangescope from the agent authority host, replacing the former hardcoded
api://AzureADTokenExchange/.default.CredentialsProvider) — resolves the bare audience from the request authorityhost, only when the caller did not set
TokenExchangeUrlexplicitly.ManagedIdentityClientAssertion) — auto-resolves the audienceper-request from the calling confidential client's authority host against MSAL's baseline
(
KnownCloudMetadata.Default), so a single shared instance still emits the correct per-cloud audience; anexplicit
tokenExchangeUrlstill wins, and the public-cloud audience is the final fallback. (This leg livesin the Certificateless assembly, which has no Abstractions dependency, so it consults only the MSAL baseline,
not a DI provider.)
Key design decisions
CloudMetadataResolution, so the audience-vs-scopechoice and the
/.defaultrule are defined exactly once; call sites never hand-build either form.ICloudMetadataProvider(from DI) is the only overridesurface; the MSAL public baseline is read directly from
KnownCloudMetadata.Defaultrather than exposed asa second DI seam. An upstream provider already outranks the baseline, so nothing is lost.
Web reads the shared key literal from whichever resolves first. MSAL and the upstream SDK never reference
each other.
authority (e.g. the managed-identity leg) resolves to the documented public-cloud default rather than
throwing, consistent with how ID Web's options can carry an empty instance string.
New public API surface
Namespace
Microsoft.Identity.Web:CloudMetadataServiceCollectionExtensions.AddCloudMetadata(this IServiceCollection, IConfiguration) : IServiceCollectionThis is the only new public API.
CloudMetadataResolutionand all consuming changes areinternal. Thenew surface consumes public types already shipped by MSAL (
KnownCloudMetadata,CloudMetadataKeyNames,TokenExchangeScope) and Microsoft.Identity.Abstractions (ICloudMetadataProvider,InMemoryCloudMetadataProvider,CloudMetadataKeyNames).Other changes
TokenAcquisition/CloudMetadataResolution.cs/.defaulttoTokenExchangeScope.FromAudienceTokenAcquisition/CloudMetadataServiceCollectionExtensions.csAddCloudMetadata(IConfiguration)— bind cloud metadata from configuration into anInMemoryCloudMetadataProviderTokenAcquisition/ServiceCollectionExtensions.csTokenAcquisition/TokenAcquisition.csICloudMetadataProviderTokenAcquisition/CredentialsProvider.csTokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.csICloudMetadataProviderthrough to the credentials providerCertificateless/ManagedIdentityClientAssertion.cstokenExchangeUrlstill wins; public-cloud default fallbackOidcFIC/OidcIdpSignedAssertionProvider.cs/.defaultliterals); holds an injectedICloudMetadataProviderOidcFIC/OidcIdpSignedAssertionLoader.csICloudMetadataProviderinto the providerTokenAcquisition/CloudMetadataResolver.csCloudMetadataResolutionTest/CloudMetadataResolutionTests.cs/.defaultidempotency, throw-on-unknown)Test/ManagedIdentityClientAssertionResolutionTests.csTest/CloudMetadataResolverTests.csTest/FederatedIdentityCaeTests.csscopeon the wireTest coverage
CloudMetadataResolutionTests+ManagedIdentityClientAssertionResolutionTests+FederatedIdentityCaeTests(net8.0):
ICloudMetadataProvider> MSAL baseline(
KnownCloudMetadata.Default); a per-call override wins over provider and baseline.InvalidOperationException; an empty authority resolves to the public-cloud default without throwing.TokenExchangeScope.FromAudienceappends/.defaultidempotently; the bare audience form stays bare.AddCloudMetadata(IConfiguration)registers a resolvable provider from a bound section.ManagedIdentityClientAssertion) auto-resolves the audience per-request from the calling client'sauthority host; explicit
tokenExchangeUrlwins; public-cloud default fallback.FederatedIdentityCaeTests(cross-cloud, pseudo-E2E): build the real OIDC-CC FIC pipeline (ID Web → MSAL)over a mocked MSAL HTTP layer and assert the credential-exchange request's
scopeequals the expectedcloud-specific audience +
/.default— for public and US Gov, default and custom-override, plus an injectedICloudMetadataProvideroverriding US Gov. The US-Gov-default case also guards the authority-vs-instancehost-resolution fix.