Add trim AOT-safe APIs for AddMicrosoftIdentityWebApi and EnableTokenAcquisitionToCallDownstreamApi#3683
Add trim AOT-safe APIs for AddMicrosoftIdentityWebApi and EnableTokenAcquisitionToCallDownstreamApi#3683anuchandy wants to merge 8 commits intoAzureAD:fix-aot-workfrom
Conversation
…, EnableTokenAcquisitionToCallDownstreamApi, defining placeholder binders
…sitionToCallDownstreamApi public api to internal
…uthenticationBuilder::EnableTokenAcquisitionToCallDownstreamApi to internal
jmprieur
left a comment
There was a problem hiding this comment.
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
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( |
There was a problem hiding this comment.
❌ 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
There was a problem hiding this comment.
Thanks - I’ll double-check PublicAPI.Unshipped.txt and fix it. Right, this class is already under a .Internal namespace
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
I don't think we need this binder. Isn't ConfidentialClientApplicationOptions provided from the MergedOptions?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
@anuchandy what is not trimmable in MicrosoftIdentityApplicationOptions?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
❌ 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. |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 && |
There was a problem hiding this comment.
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));
..|
@anuchandy - can we close this ? I think we took a different direction here. |
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.jsonandCredentials.json, which serve as the source of truth for the configuration contract.API Changes
AddMicrosoftIdentityWebApi(configuration, configSectionName, jwtBearerScheme, subscribeToEvents)AddMicrosoftIdentityWebApiTrimSafe(configuration, configSectionName, jwtBearerScheme, subscribeToEvents)EnableTokenAcquisitionToCallDownstreamApi()EnableTokenAcquisitionToCallDownstreamApiTrimSafe()How It Works
The
TrimSafeAPIs internally use hand-written configuration binders that avoid reflection, making them compatible with Native AOT and trimming scenarios.Usage