Skip to content

Resolve cloud-specific metadata by authority host - #3994

Merged
Avery-Dunn merged 16 commits into
masterfrom
avdunn/cloud-configuration
Sep 14, 2026
Merged

Avery-Dunn merged 16 commits into
masterfrom
avdunn/cloud-configuration

Conversation

@Avery-Dunn

@Avery-Dunn Avery-Dunn commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

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://AzureADTokenExchangeUSGov
for US Government) rather than the public-cloud api://AzureADTokenExchange. ID Web previously hardcoded the
public 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 ICloudMetadataProvider resolved from DI and passes it in per call. Resolution layers,
highest precedence first:

  1. an explicit per-call override supplied by the caller (e.g. a credential's TokenExchangeUrl);
  2. an upstream ICloudMetadataProvider from DI — contributed by a caller or an upstream SDK; this is how
    internal-only sovereign clouds (which MSAL does not ship) become resolvable;
  3. MSAL's built-in public baseline, read directly from 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-token
    contexts. The /.default computation is delegated to MSAL's TokenExchangeScope.FromAudience, the single
    cross-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.FederatedCredentialAudience equals MSAL's
CloudMetadataKeyNames.FederatedCredentialAudience), ID Web "translates" between the two SDKs simply by
reading 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
ICloudMetadataProvider from the container (so the winning TryAdd provider 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
InvalidOperationException naming the host and pointing at the override APIs (AddCloudMetadata(...), an
ICloudMetadataProvider registration, or an explicit TokenExchangeUrl) — rather than silently exchanging
against 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.json section:

services.AddCloudMetadata(configuration.GetSection("CloudMetadata"));
"CloudMetadata": {
  "login.microsoftonline.us": { "federated_credential_audience": "api://AzureADTokenExchangeUSGov" },
  "login.mynewcloud.example": { "federated_credential_audience": "api://AzureADTokenExchangeMyCloud" }
}

It binds each host→key/value section into an Abstractions InMemoryCloudMetadataProvider registered as
ICloudMetadataProvider via TryAddSingleton (so an explicitly registered provider — including an upstream
SDK'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:

  • OIDC client-credentials FIC (OidcIdpSignedAssertionProvider) — resolves the exchange scope from
    the application's instance/authority host; an explicitly configured TokenExchangeUrl is passed as the
    per-call override. The provider is injected via an internal constructor overload (from DI).
  • Agent identity FIC (TokenAcquisition, blueprint leg 1 + instance leg 2) — resolves the exchange
    scope from the agent authority host, replacing the former hardcoded api://AzureADTokenExchange/.default.
  • Managed-identity FIC (CredentialsProvider) — resolves the bare audience from the request authority
    host, only when the caller did not set TokenExchangeUrl explicitly.
  • Managed-identity client assertion (ManagedIdentityClientAssertion) — auto-resolves the audience
    per-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; an
    explicit tokenExchangeUrl still wins, and the public-cloud audience is the final fallback. (This leg lives
    in the Certificateless assembly, which has no Abstractions dependency, so it consults only the MSAL baseline,
    not a DI provider.)

Key design decisions

  1. One decision point. All FIC legs route through CloudMetadataResolution, so the audience-vs-scope
    choice and the /.default rule are defined exactly once; call sites never hand-build either form.
  2. A single override seam. The Abstractions ICloudMetadataProvider (from DI) is the only override
    surface; the MSAL public baseline is read directly from KnownCloudMetadata.Default rather than exposed as
    a second DI seam. An upstream provider already outranks the baseline, so nothing is lost.
  3. No cross-SDK dependency. MSAL owns public cloud strings; an upstream SDK owns internal-only strings; ID
    Web reads the shared key literal from whichever resolves first. MSAL and the upstream SDK never reference
    each other.
  4. Host-keyed; empty authority is not an error. Resolution keys off the extracted host; a null/empty
    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:

API Kind
CloudMetadataServiceCollectionExtensions.AddCloudMetadata(this IServiceCollection, IConfiguration) : IServiceCollection static ext

This is the only new public API. CloudMetadataResolution and all consuming changes are internal. The
new surface consumes public types already shipped by MSAL (KnownCloudMetadata, CloudMetadataKeyNames,
TokenExchangeScope) and Microsoft.Identity.Abstractions (ICloudMetadataProvider,
InMemoryCloudMetadataProvider, CloudMetadataKeyNames).

Other changes

File Description
TokenAcquisition/CloudMetadataResolution.cs New internal static resolver — the single audience/scope decision point (per-call → upstream provider → MSAL baseline → throw); delegates /.default to TokenExchangeScope.FromAudience
TokenAcquisition/CloudMetadataServiceCollectionExtensions.cs New public AddCloudMetadata(IConfiguration) — bind cloud metadata from configuration into an InMemoryCloudMetadataProvider
TokenAcquisition/ServiceCollectionExtensions.cs Removed the old resolver-singleton factory registration (resolver is now static; providers resolved from DI directly)
TokenAcquisition/TokenAcquisition.cs Agent FIC legs 1 & 2 resolve the exchange scope by authority host via the static resolver + a DI ICloudMetadataProvider
TokenAcquisition/CredentialsProvider.cs Managed-identity FIC leg resolves the bare audience by authority host (DI provider) when none is set explicitly
TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs Flows the DI ICloudMetadataProvider through to the credentials provider
Certificateless/ManagedIdentityClientAssertion.cs Per-request audience auto-resolved from the calling client's authority host (MSAL baseline); explicit tokenExchangeUrl still wins; public-cloud default fallback
OidcFIC/OidcIdpSignedAssertionProvider.cs Resolves the exchange scope via the static resolver (removes the local default + /.default literals); holds an injected ICloudMetadataProvider
OidcFIC/OidcIdpSignedAssertionLoader.cs Passes the DI ICloudMetadataProvider into the provider
TokenAcquisition/CloudMetadataResolver.cs Deleted — replaced by the static CloudMetadataResolution
Test/CloudMetadataResolutionTests.cs New unit tests for the static resolver (precedence, per-call override, /.default idempotency, throw-on-unknown)
Test/ManagedIdentityClientAssertionResolutionTests.cs New unit tests for the MI-leg per-request audience resolution
Test/CloudMetadataResolverTests.cs Deleted — superseded
Test/FederatedIdentityCaeTests.cs Cross-cloud FIC exchange tests asserting the credential-exchange scope on the wire

Test coverage

CloudMetadataResolutionTests + ManagedIdentityClientAssertionResolutionTests + FederatedIdentityCaeTests
(net8.0):

  • Precedence/layering: per-call override > upstream ICloudMetadataProvider > MSAL baseline
    (KnownCloudMetadata.Default); a per-call override wins over provider and baseline.
  • Known sovereign hosts resolve from the MSAL baseline; a non-empty host unknown to every source throws
    InvalidOperationException; an empty authority resolves to the public-cloud default without throwing.
  • TokenExchangeScope.FromAudience appends /.default idempotently; the bare audience form stays bare.
  • AddCloudMetadata(IConfiguration) registers a resolvable provider from a bound section.
  • MI-leg (ManagedIdentityClientAssertion) auto-resolves the audience per-request from the calling client's
    authority host; explicit tokenExchangeUrl wins; 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 scope equals the expected
    cloud-specific audience + /.default — for public and US Gov, default and custom-override, plus an injected
    ICloudMetadataProvider overriding US Gov. The US-Gov-default case also guards the authority-vs-instance
    host-resolution fix.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 CloudMetadataResolver and 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.

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated
Comment thread src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

  • scenario is currently unused in RunFicExchangeScenarioAsync. 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.

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs Outdated
Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CloudMetadataResolver.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • src/Microsoft.Identity.Web.UI/Microsoft.Identity.Web.UI.xml: Generated file

Comment thread src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/Microsoft.Identity.Web.UI/Microsoft.Identity.Web.UI.xml: Generated file

Comment thread tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs
Avery-Dunn and others added 8 commits September 10, 2026 16:11
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>
@Avery-Dunn
Avery-Dunn force-pushed the avdunn/cloud-configuration branch from 40496bd to 535d8e8 Compare September 10, 2026 23:16
@Avery-Dunn
Avery-Dunn merged commit ec50a7b into master Sep 14, 2026
9 checks passed
@Avery-Dunn
Avery-Dunn deleted the avdunn/cloud-configuration branch September 14, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants