Skip to content

Add trim AOT-safe APIs for AddMicrosoftIdentityWebApi and EnableTokenAcquisitionToCallDownstreamApi#3683

Open
anuchandy wants to merge 8 commits intoAzureAD:fix-aot-workfrom
anuchandy:aot-apis-2
Open

Add trim AOT-safe APIs for AddMicrosoftIdentityWebApi and EnableTokenAcquisitionToCallDownstreamApi#3683
anuchandy wants to merge 8 commits intoAzureAD:fix-aot-workfrom
anuchandy:aot-apis-2

Conversation

@anuchandy
Copy link
Collaborator

Summary

This PR adds two trim-AOT-compatible APIs (to support Azure MCP Server trimming, which is required for azmcp 2.0 GA targeting mid-April).

These new APIs avoid reflection-based ConfigurationBinder.Bind() by using custom configuration binders, enabling Native AOT compilation and trimming.

These custom binders binds the properties defined in the repository's JSON schema files microsoft-identity-web.json and Credentials.json, which serve as the source of truth for the configuration contract.

API Changes

Original API (unchanged) Trim-Safe API (new)
AddMicrosoftIdentityWebApi(configuration, configSectionName, jwtBearerScheme, subscribeToEvents) AddMicrosoftIdentityWebApiTrimSafe(configuration, configSectionName, jwtBearerScheme, subscribeToEvents)
EnableTokenAcquisitionToCallDownstreamApi() EnableTokenAcquisitionToCallDownstreamApiTrimSafe()

How It Works

The TrimSafe APIs internally use hand-written configuration binders that avoid reflection, making them compatible with Native AOT and trimming scenarios.

Usage

// Using in Azure MCP Server or any web api app adhering to AOT compilance
//
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApiTrimSafe(builder.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApiTrimSafe();

@anuchandy anuchandy requested a review from a team as a code owner January 27, 2026 17:17
Copy link
Collaborator

@jmprieur jmprieur left a comment

Choose a reason for hiding this comment

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

Summary of the approach:

flowchart LR
    subgraph "New TrimSafe APIs"
        A[AddMicrosoftIdentityWebApiTrimSafe] --> B[Hand-written Binders]
        C[EnableTokenAcquisitionToCallDownstreamApiTrimSafe] --> B
    end
    
    subgraph "Hand-written Binders (No Reflection)"
        B --> D[JwtBearerOptionsBinder]
        B --> E[MicrosoftIdentityOptionsBinder]
        B --> F[MicrosoftIdentityApplicationOptionsBinder]
        B --> G[ConfidentialClientApplicationOptionsBinder]
        B --> H[CredentialDescriptionBinder]
    end
    
    subgraph "Original APIs (Unchanged)"
        I[AddMicrosoftIdentityWebApi] --> J["ConfigurationBinder.Bind() ❌ AOT"]
        K[EnableTokenAcquisitionToCallDownstreamApi] --> J
    end
Loading

This is fragile.
I see high risk in the manual binder maintenance burden because we know these classes evolve: If MicrosoftIdentityOptions, MicrosoftIdentityApplicationOptions, CredentialDescription, etc. gain new properties in future releases of Microsoft.Identity.Abstractions or IdWeb, the binders will silently ignore them unless manually updated.

Mitigation needed: Unit tests that verify all public properties are bound unless we don't want them

/// <param name="services">The services being configured.</param>
/// <param name="configuration">IConfigurationSection.</param>
/// <returns>The authentication builder to chain.</returns>
public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisition(
Copy link
Collaborator

Choose a reason for hiding this comment

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

❌ Despite the XML doc saying "This API is not considered part of the public API and may change", it's marked public static and added to PublicAPI.Unshipped.txt

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks - I’ll double-check PublicAPI.Unshipped.txt and fix it. Right, this class is already under a .Internal namespace

Copy link
Collaborator Author

@anuchandy anuchandy Jan 28, 2026

Choose a reason for hiding this comment

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

I Checked this. The WebApiBuilders class is declared as public static, which means its public methods are technically part of the public API surface from the analyzer's perspective. The Microsoft.Identity.Web.Internal namespace here is a naming convention to signal to devs that this API should not be used, but it doesn't change the actual C# accessibility level.

The code change here follows the existing pattern - the original EnableTokenAcquisition method (the AOT-unsafe one) with the same XML comment is already in the PublicAPI.Shipped.txt.

/// </summary>
/// <param name="options">The options instance to bind to.</param>
/// <param name="configurationSection">The configuration section containing the values.</param>
public static void Bind(ConfidentialClientApplicationOptions options, IConfigurationSection? configurationSection)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need this binder. Isn't ConfidentialClientApplicationOptions provided from the MergedOptions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thanks, to enable EnableTokenAcquisitionToCallDownstreamApiTrimSafe(), I was following the same steps as the existing AOT-unsafe EnableTokenAcquisitionToCallDownstreamApi(). As we can see in the current (AOT-unsafe) implementation, it does bind into ConfidentialClientApplicationOptions. I’ve captured that AOT-unsafe flow below.

To support the same logic in the new AOT-safe equivalent, we introduced ConfidentialClientApplicationOptionsBinder to perform the binding.

With my limited familiarity with this package, I’m not sure whether the existing code was doing this unnecessarily- but I was aiming to stay consistent with the current behavior.

┌─────────────────────────────────────────────────────────────────────────────────┐
│  MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration                  │
│                                                                                 │
│  EnableTokenAcquisitionToCallDownstreamApi()                                    │
│                                                                                 │
│    return EnableTokenAcquisitionToCallDownstreamApi(                            │
│               options => ConfigurationSection?.Bind(options));                  │
└─────────────────────────────────────────┬───────────────────────────────────────┘
                                          │
                                          ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│  MicrosoftIdentityWebApiAuthenticationBuilder                                   │
│                                                                                 │
│  EnableTokenAcquisitionToCallDownstreamApi(                                     │
│      Action<ConfidentialClientApplicationOptions>                               │
│          configureConfidentialClientApplicationOptions)                         │
│                                                                                 │
│    CallsWebApiImplementation(                                                   │
│        Services,                                                                │
│        JwtBearerAuthenticationScheme,                                           │
│        configureConfidentialClientApplicationOptions,                           │
│        ConfigurationSection);                                                   │
└─────────────────────────────────────────┬───────────────────────────────────────┘
                                          │
                                          ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│  MicrosoftIdentityWebApiAuthenticationBuilder                                   │
│                                                                                 │
│  CallsWebApiImplementation(...)                                                 │
│                                                                                 │
│    services.Configure(jwtBearerAuthenticationScheme,                            │
│                       configureConfidentialClientApplicationOptions);           │
│                       ▲                                                         │
│                       │                                                         │
│         Registers: options => ConfigurationSection?.Bind(options)               │
│                    (binds ConfidentialClientApplicationOptions)                 │
└─────────────────────────────────────────────────────────────────────────────────┘

/// AOT-safe binder for <see cref="CredentialDescription"/>.
/// Binds configuration values based on the JSON schema defined in Credentials.json.
/// </summary>
internal static class CredentialDescriptionBinder
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some properties are not bound (for instance the AutoDecrypt credentials)
If we want to go this route (which is fragile), we'd need to have serious tests to check that all the properties we want are correctly bound.

Also this should go in Microsoft.Identity.Abstractions if we want to do that

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure - I’ll work on adding tests for all the binders. I’ll also take a look at the other repo; I wasn’t aware the work spans two repos/packages, so thanks for the pointer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps, unit tests with reflection (so it can iterate over all properties of option class to see whether its bound to configuration) can alleviate it in case if this path to bind is chosen.

/// on <see cref="MicrosoftIdentityApplicationOptions"/> and its base classes
/// (<see cref="MicrosoftEntraApplicationOptions"/> and <see cref="IdentityApplicationOptions"/>).
/// </summary>
internal static class MicrosoftIdentityApplicationOptionsBinder
Copy link
Collaborator

Choose a reason for hiding this comment

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

@anuchandy what is not trimmable in MicrosoftIdentityApplicationOptions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The MicrosoftIdentityApplicationOptions type itself doesn't have any inherently untrimmable properties - all its properties (including inherited ones from MicrosoftEntraApplicationOptions and IdentityApplicationOptions) are bound by this hand-written binder.

What is trim/AOT-unfriendly is the original use of ConfigurationBinder.Bind() to populate this type.

/// </summary>
/// <param name="options">The options instance to bind to.</param>
/// <param name="configurationSection">The configuration section containing the values.</param>
public static void Bind(MicrosoftIdentityOptions options, IConfigurationSection? configurationSection)
Copy link
Collaborator

Choose a reason for hiding this comment

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

❌ This binder has its own BindCertificateDescription and BindCertificateDescriptionCollection methods, while CredentialDescriptionBinder exists separately. There's partial overlap but not full reuse (CertificatelDescription inherits from CredentialDescription)

}

// Note: DecryptKeysAuthenticationOptions is a complex nested type used for AutoDecryptKeys.
// Skipping for now as it requires a separate binder and is less commonly used.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we create a work item with providing a link there to do not forget about it?


// ClientSecret - for SourceType = ClientSecret
var clientSecret = configurationSection[nameof(CredentialDescription.ClientSecret)];
if (!string.IsNullOrEmpty(clientSecret))
Copy link
Contributor

Choose a reason for hiding this comment

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

In case if clientSecret is empty string, options.ClientSecret will contain old value - is it expected? It applies thorough.

options.Instance = instance;
}

if (configurationSection[nameof(options.AadAuthorityAudience)] is string aadAuthorityAudience &&
Copy link
Contributor

Choose a reason for hiding this comment

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

There are some repetitive blocks of code which can be made more declarative, can we consider extracting it to helper functions with calling it something like below? As benefit, there will be a bit clearer bind code and each bind util method can be granularly unit tested.

..
BinderUtils.BindEnum<AadAuthorityAudience>(options, configurationSection, nameof(options.AadAuthorityAudience));
BinderUtils.BindEnum<AzureCloudInstance>(options, configurationSection, nameof(options.AzureCloudInstance));
BinderUtils.BindString(options, configurationSection, nameof(options.RedirectUri));
..

@bgavrilMS
Copy link
Member

@anuchandy - can we close this ? I think we took a different direction here.

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