diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj b/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj
index 556755b08..ebf5a85dd 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj
@@ -8,15 +8,10 @@
README.md
-
+
true
-
-
-
@@ -39,7 +34,7 @@
\
-
+
@@ -66,5 +61,5 @@
-
+
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt
index b16d79cf0..ac3d3ed16 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt
@@ -6,6 +6,11 @@ const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" ->
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task!
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger
+Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsStore) -> void
+Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void
+Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void
+Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task!
static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void
static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet!
static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet!
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt
index b16d79cf0..4bcd2595f 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt
@@ -6,6 +6,9 @@ const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" ->
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task!
Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.Internal.MicrosoftIdentityOptionsBinder
+Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void
+Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task!
static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void
static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet!
static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet!
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs b/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs
index c482a62f5..eab099a5a 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs
@@ -36,8 +36,6 @@ public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAc
{
if (configuration != null)
{
- // TODO: This never was right. And the configureConfidentialClientApplicationOptions delegate is not used
- // services.Configure(authenticationScheme, configuration);
services.Configure(authenticationScheme, options
=>
{ configuration.Bind(options); });
diff --git a/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs b/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs
new file mode 100644
index 000000000..6623fc91b
--- /dev/null
+++ b/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Identity.Abstractions;
+using Microsoft.IdentityModel.Tokens;
+
+#if !NETSTANDARD2_0 && !NET462 && !NET472
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Identity.Web.Resource;
+#endif
+
+namespace Microsoft.Identity.Web.Internal
+{
+ ///
+ /// Shared helper methods for identity options configuration and validation.
+ /// Used by both traditional (MergedOptions) and AOT-compatible paths.
+ ///
+ internal static class IdentityOptionsHelpers
+ {
+ ///
+ /// Builds the authority URL from the given application options.
+ /// Handles AAD, B2C, and CIAM scenarios.
+ ///
+ /// The application options containing instance, tenant, and domain information.
+ /// The constructed authority URL.
+ internal static string BuildAuthority(MicrosoftIdentityApplicationOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Instance))
+ {
+ throw new ArgumentNullException(nameof(options.Instance));
+ }
+
+#if !NETSTANDARD2_0 && !NET462 && !NET472
+ Uri baseUri = new Uri(options.Instance);
+ var domain = options.Domain;
+ var tenantId = options.TenantId;
+
+ // B2C is detected by presence of SignUpSignInPolicyId
+ bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId);
+
+ if (isB2C)
+ {
+ var userFlow = options.SignUpSignInPolicyId;
+ return new Uri(baseUri, new PathString($"{baseUri.PathAndQuery}{domain}/{userFlow}/v2.0")).ToString();
+ }
+
+ return new Uri(baseUri, new PathString($"{baseUri.PathAndQuery}{tenantId}/v2.0")).ToString();
+#else
+ // For non-ASP.NET Core, use simple string concatenation
+ // options.Instance is guaranteed to be non-null because we check it at the start of the method
+ var instance = options.Instance!.TrimEnd('/');
+ bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId);
+
+ if (isB2C)
+ {
+ return $"{instance}/{options.Domain}/{options.SignUpSignInPolicyId}/v2.0";
+ }
+
+ return $"{instance}/{options.TenantId}/v2.0";
+#endif
+ }
+
+
+#if !NETSTANDARD2_0 && !NET462 && !NET472
+ ///
+ /// Configures issuer validation on the JWT bearer options.
+ /// Sets up multi-tenant issuer validation logic that accepts both v1.0 and v2.0 tokens.
+ /// If the developer has already registered an IssuerValidator, it will not be overwritten.
+ ///
+ /// The JWT bearer options containing token validation parameters and authority.
+ /// The service provider to resolve the issuer validator factory.
+ internal static void ConfigureIssuerValidation(
+ JwtBearerOptions options,
+ IServiceProvider serviceProvider)
+ {
+ if (options.TokenValidationParameters.ValidateIssuer &&
+ options.TokenValidationParameters.IssuerValidator == null)
+ {
+ var microsoftIdentityIssuerValidatorFactory =
+ serviceProvider.GetRequiredService();
+
+ options.TokenValidationParameters.IssuerValidator =
+ microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority!).Validate;
+ }
+ }
+
+ ///
+ /// Ensures the JwtBearerOptions.Events object exists and wires up the
+ /// ConfigurationManager on the OnMessageReceived event.
+ ///
+ /// The JWT bearer options to configure.
+ internal static void InitializeJwtBearerEvents(JwtBearerOptions options)
+ {
+ if (options.Events == null)
+ {
+ options.Events = new JwtBearerEvents();
+ }
+
+ var existingOnMessageReceived = options.Events.OnMessageReceived;
+ options.Events.OnMessageReceived = async context =>
+ {
+ context.Options.TokenValidationParameters.ConfigurationManager ??=
+ options.ConfigurationManager as BaseConfigurationManager;
+
+ if (existingOnMessageReceived != null)
+ {
+ await existingOnMessageReceived(context).ConfigureAwait(false);
+ }
+ };
+ }
+
+ ///
+ /// Configures audience validation on the token validation parameters if not already configured.
+ /// Sets up custom validator for handling v1.0/v2.0 and B2C tokens correctly.
+ /// This is AOT-compatible as it directly sets up the validator without using reflection or MicrosoftIdentityOptions.
+ ///
+ /// The token validation parameters to configure.
+ /// The application (client) ID.
+ /// Whether the application targets Azure AD B2C.
+ internal static void ConfigureAudienceValidation(
+ TokenValidationParameters validationParameters,
+ string? clientId,
+ bool isB2C)
+ {
+ // Skip if audience validation is already configured by the caller
+ if (validationParameters.AudienceValidator != null ||
+ validationParameters.ValidAudience != null ||
+ validationParameters.ValidAudiences != null)
+ {
+ return;
+ }
+
+ // Set up the audience validator directly without converting to MicrosoftIdentityOptions
+ validationParameters.AudienceValidator = (audiences, securityToken, validationParams) =>
+ {
+ var claims = securityToken switch
+ {
+ System.IdentityModel.Tokens.Jwt.JwtSecurityToken jwtSecurityToken => jwtSecurityToken.Claims,
+ Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtWebToken => jwtWebToken.Claims,
+ _ => throw new SecurityTokenValidationException(IDWebErrorMessage.TokenIsNotJwtToken),
+ };
+
+ validationParams.AudienceValidator = null;
+
+ // Case of a default App ID URI (the developer did not provide explicit valid audience(s))
+ if (string.IsNullOrEmpty(validationParams.ValidAudience) &&
+ validationParams.ValidAudiences == null)
+ {
+ // handle v2.0 access token or Azure AD B2C tokens (even if v1.0)
+ if (isB2C || claims.Any(c => c.Type == Constants.Version && c.Value == Constants.V2))
+ {
+ validationParams.ValidAudience = $"{clientId}";
+ }
+ // handle v1.0 access token
+ else if (claims.Any(c => c.Type == Constants.Version && c.Value == Constants.V1))
+ {
+ validationParams.ValidAudience = $"api://{clientId}";
+ }
+ }
+
+ Validators.ValidateAudience(audiences, securityToken, validationParams);
+ return true;
+ };
+ }
+
+ ///
+ /// Chains a handler onto the OnTokenValidated event to store the token for OBO scenarios.
+ ///
+ /// The existing OnTokenValidated handler, if any.
+ /// A new handler that stores the token and then calls the existing handler.
+ internal static Func ChainTokenStorageHandler(Func? existingHandler)
+ {
+ return async context =>
+ {
+ // Only pass through a token if it is of an expected type
+ context.HttpContext.StoreTokenUsedToCallWebAPI(
+ context.SecurityToken is System.IdentityModel.Tokens.Jwt.JwtSecurityToken or
+ Microsoft.IdentityModel.JsonWebTokens.JsonWebToken ? context.SecurityToken : null);
+
+ if (existingHandler != null)
+ {
+ await existingHandler(context).ConfigureAwait(false);
+ }
+ };
+ }
+#endif
+ }
+}
diff --git a/src/Microsoft.Identity.Web/Policy/RequiredScopeOrAppPermissionExtensions.cs b/src/Microsoft.Identity.Web/Policy/RequiredScopeOrAppPermissionExtensions.cs
index fc8e8b278..fc81ba553 100644
--- a/src/Microsoft.Identity.Web/Policy/RequiredScopeOrAppPermissionExtensions.cs
+++ b/src/Microsoft.Identity.Web/Policy/RequiredScopeOrAppPermissionExtensions.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -22,8 +21,6 @@ public static class RequiredScopeOrAppPermissionExtensions
///
/// The services being configured.
/// Services.
- [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")]
- [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")]
public static IServiceCollection AddRequiredScopeOrAppPermissionAuthorization(this IServiceCollection services)
{
services.AddAuthorization();
diff --git a/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs b/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs
index 82740ddff..0cbb1f275 100644
--- a/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs
+++ b/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@@ -18,8 +17,6 @@ namespace Microsoft.Identity.Web
/// Scope or app permission authorization handler that needs to be called for a specific requirement type.
/// In this case, .
///
- [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")]
- [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")]
internal class ScopeOrAppPermissionAuthorizationHandler : AuthorizationHandler
{
private readonly IConfiguration _configuration;
@@ -77,7 +74,7 @@ protected override Task HandleRequirementAsync(
if (appPermissionConfigurationKey != null)
{
- appPermissions = _configuration.GetValue(appPermissionConfigurationKey)?.Split(' ');
+ appPermissions = _configuration[appPermissionConfigurationKey]?.Split(' ');
}
if (appPermissions is null)
diff --git a/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs b/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs
new file mode 100644
index 000000000..470daa442
--- /dev/null
+++ b/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs
@@ -0,0 +1,166 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NET10_0_OR_GREATER
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.Internal;
+using Microsoft.Identity.Web.Resource;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.IdentityModel.Validators;
+
+namespace Microsoft.Identity.Web.PostConfigureOptions
+{
+ ///
+ /// Post-configures JwtBearerOptions for AOT-compatible path using MicrosoftIdentityApplicationOptions.
+ /// Performs validation, configuration, and OBO token storage setup.
+ ///
+ internal sealed class MicrosoftIdentityJwtBearerOptionsPostConfigurator : IPostConfigureOptions
+ {
+ private readonly IOptionsMonitor _appOptionsMonitor;
+ private readonly IServiceProvider _serviceProvider;
+
+ public MicrosoftIdentityJwtBearerOptionsPostConfigurator(
+ IOptionsMonitor appOptionsMonitor,
+ IServiceProvider serviceProvider)
+ {
+ _appOptionsMonitor = appOptionsMonitor;
+ _serviceProvider = serviceProvider;
+ }
+
+ public void PostConfigure(string? name, JwtBearerOptions options)
+ {
+ var appOptions = _appOptionsMonitor.Get(name ?? string.Empty);
+
+ // Skip if not configured via our AOT path (no ClientId means not configured)
+ if (string.IsNullOrEmpty(appOptions.ClientId))
+ {
+ return;
+ }
+
+ // 1. VALIDATE (fail-fast with complete configuration)
+ ValidateRequiredOptions(appOptions);
+
+ // 2. CONFIGURE (respect customer overrides)
+
+ // Note: 'options.Authority' is set during the Configure phase in AddMicrosoftIdentityWebApiAot,
+ // before any PostConfigure runs to ensure ASP.NET's built-in JwtBearerPostConfigureOptions can
+ // create the ConfigurationManager from it.
+
+ // Configure audience validation if not already set
+ IdentityOptionsHelpers.ConfigureAudienceValidation(
+ options.TokenValidationParameters,
+ appOptions.ClientId,
+ !string.IsNullOrWhiteSpace(appOptions.SignUpSignInPolicyId));
+
+ // Configure issuer validation
+ IdentityOptionsHelpers.ConfigureIssuerValidation(options, _serviceProvider);
+
+ // Configure token decryption if credentials provided
+ if (appOptions.TokenDecryptionCredentials != null && appOptions.TokenDecryptionCredentials.Any())
+ {
+ // Extract user assigned identity client ID from credentials if present
+ var managedIdentityCredential = appOptions.TokenDecryptionCredentials
+ .OfType()
+ .FirstOrDefault(c => !string.IsNullOrEmpty(c.ManagedIdentityClientId));
+
+ if (managedIdentityCredential != null)
+ {
+ DefaultCertificateLoader.UserAssignedManagedIdentityClientId = managedIdentityCredential.ManagedIdentityClientId;
+ }
+
+ IEnumerable certificates = DefaultCertificateLoader.LoadAllCertificates(
+ appOptions.TokenDecryptionCredentials.OfType());
+ IEnumerable keys = certificates.Select(c => new X509SecurityKey(c));
+ options.TokenValidationParameters.TokenDecryptionKeys = keys;
+ }
+
+ // Enable AAD signing key issuer validation
+ options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();
+
+ // Ensure events and wire up ConfigurationManager on message received
+ IdentityOptionsHelpers.InitializeJwtBearerEvents(options);
+
+ // Add claims validation if not allowing ACL authorization
+ if (!appOptions.AllowWebApiToBeAuthorizedByACL)
+ {
+ MicrosoftIdentityWebApiAuthenticationBuilderExtensions.ChainOnTokenValidatedEventForClaimsValidation(
+ options.Events, name ?? JwtBearerDefaults.AuthenticationScheme);
+ }
+
+ // =========================================================
+ // 3. CHAIN OnTokenValidated (always - required for OBO)
+ // =========================================================
+ options.Events.OnTokenValidated = IdentityOptionsHelpers.ChainTokenStorageHandler(
+ options.Events.OnTokenValidated);
+ }
+
+ ///
+ /// Validates that required options are present based on the configuration scenario.
+ ///
+ /// The application options to validate.
+ /// Thrown when required options are missing.
+ internal static void ValidateRequiredOptions(MicrosoftIdentityApplicationOptions options)
+ {
+ if (string.IsNullOrEmpty(options.ClientId))
+ {
+ throw new ArgumentNullException(
+ nameof(options.ClientId),
+ string.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ IDWebErrorMessage.ConfigurationOptionRequired,
+ nameof(options.ClientId)));
+ }
+
+ // B2C is detected by presence of SignUpSignInPolicyId
+ bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId);
+
+ if (string.IsNullOrEmpty(options.Authority))
+ {
+ if (string.IsNullOrEmpty(options.Instance))
+ {
+ throw new ArgumentNullException(
+ nameof(options.Instance),
+ string.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ IDWebErrorMessage.ConfigurationOptionRequired,
+ nameof(options.Instance)));
+ }
+
+ if (isB2C)
+ {
+ if (string.IsNullOrEmpty(options.Domain))
+ {
+ throw new ArgumentNullException(
+ nameof(options.Domain),
+ string.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ IDWebErrorMessage.ConfigurationOptionRequired,
+ nameof(options.Domain)));
+ }
+ }
+ else
+ {
+ if (string.IsNullOrEmpty(options.TenantId))
+ {
+ throw new ArgumentNullException(
+ nameof(options.TenantId),
+ string.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ IDWebErrorMessage.ConfigurationOptionRequired,
+ nameof(options.TenantId)));
+ }
+ }
+ }
+ }
+ }
+}
+
+#endif
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Unshipped.txt
index 7dc5c5811..4dee6ecc3 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Unshipped.txt
@@ -1 +1,12 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index 7dc5c5811..b1b6c4017 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApiAot(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action! configureOptions, string! jwtBearerScheme, System.Action? configureJwtBearerOptions) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Unshipped.txt
index 7dc5c5811..704f2a961 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Unshipped.txt
@@ -1 +1,8 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Unshipped.txt
index 7dc5c5811..704f2a961 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Unshipped.txt
@@ -1 +1,8 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt
index 7dc5c5811..5a9101eb1 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Unshipped.txt
@@ -1 +1,11 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt
index 7dc5c5811..5a9101eb1 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Unshipped.txt
@@ -1 +1,11 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+
diff --git a/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt
index 7dc5c5811..704f2a961 100644
--- a/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt
@@ -1 +1,8 @@
#nullable enable
+Microsoft.Identity.Web.Internal.IdentityOptionsHelpers
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void
+Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string!
+static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func!
+
diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs
index b321d7424..f93ee1cf5 100644
--- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs
+++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs
@@ -93,14 +93,8 @@ internal static void CallsWebApiImplementation(
{
options.Events ??= new JwtBearerEvents();
- var onTokenValidatedHandler = options.Events.OnTokenValidated;
-
- options.Events.OnTokenValidated = async context =>
- {
- // Only pass through a token if it is of an expected type
- context.HttpContext.StoreTokenUsedToCallWebAPI(context.SecurityToken is JwtSecurityToken or JsonWebToken ? context.SecurityToken : null);
- await onTokenValidatedHandler(context).ConfigureAwait(false);
- };
+ // Chain token storage handler using shared helper
+ options.Events.OnTokenValidated = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(options.Events.OnTokenValidated);
});
}
}
diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs
new file mode 100644
index 000000000..ed3092b17
--- /dev/null
+++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NET10_0_OR_GREATER
+
+using System;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.Internal;
+using Microsoft.Identity.Web.PostConfigureOptions;
+using Microsoft.Identity.Web.Resource;
+
+namespace Microsoft.Identity.Web
+{
+ ///
+ /// Extensions for for startup initialization of web APIs (AOT-compatible).
+ ///
+ public static partial class MicrosoftIdentityWebApiAuthenticationBuilderExtensions
+ {
+ ///
+ /// Protects the web API with Microsoft identity platform (AOT-compatible).
+ /// This is the AOT-safe alternative to
+ /// and does not rely on reflection-based configuration binding.
+ ///
+ /// The to which to add this configuration.
+ /// The action to configure .
+ /// The JWT bearer scheme name to be used. By default it uses "Bearer".
+ /// Optional action to configure .
+ /// The authentication builder to chain.
+ ///
+ ///
+ /// This method takes an delegate that
+ /// the caller uses to bind configuration values.
+ ///
+ ///
+ /// To get AOT-safe configuration binding, enable the configuration binding source generator
+ /// in your project file:
+ ///
+ ///
+ /// <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
+ ///
+ ///
+ /// The source generator produces compile-time binding code for
+ ///
+ /// calls, eliminating the need for reflection at runtime.
+ ///
+ ///
+ ///
+ /// The following example shows how to protect a web API using AOT-compatible configuration binding:
+ ///
+ /// var azureAdSection = builder.Configuration.GetSection("AzureAd");
+ ///
+ /// builder.Services
+ /// .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ /// .AddMicrosoftIdentityWebApiAot(
+ /// options => azureAdSection.Bind(options),
+ /// JwtBearerDefaults.AuthenticationScheme,
+ /// configureJwtBearerOptions: null);
+ ///
+ ///
+ public static AuthenticationBuilder AddMicrosoftIdentityWebApiAot(
+ this AuthenticationBuilder builder,
+ Action configureOptions,
+ string jwtBearerScheme,
+ Action? configureJwtBearerOptions)
+ {
+ _ = Throws.IfNull(builder);
+ _ = Throws.IfNull(configureOptions);
+
+ // Register MicrosoftIdentityApplicationOptions
+ builder.Services.Configure(jwtBearerScheme, configureOptions);
+
+ // Add JWT Bearer with optional custom configuration
+ if (configureJwtBearerOptions != null)
+ {
+ builder.AddJwtBearer(jwtBearerScheme, configureJwtBearerOptions);
+ }
+ else
+ {
+ builder.AddJwtBearer(jwtBearerScheme);
+ }
+
+ // Set Authority during Configure phase so that ASP.NET's built-in JwtBearerPostConfigureOptions can create the ConfigurationManager from it.
+ builder.Services.AddOptions(jwtBearerScheme)
+ .Configure>((jwtOptions, appOptionsMonitor) =>
+ {
+ if (!string.IsNullOrEmpty(jwtOptions.Authority))
+ {
+ return;
+ }
+
+ var appOptions = appOptionsMonitor.Get(jwtBearerScheme);
+ if (string.IsNullOrEmpty(appOptions.ClientId))
+ {
+ // Skip if not configured via AOT path (no ClientId means not configured)
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(appOptions.Authority))
+ {
+ var authority = AuthorityHelpers.BuildCiamAuthorityIfNeeded(appOptions.Authority, out _);
+ jwtOptions.Authority = AuthorityHelpers.EnsureAuthorityIsV2(authority ?? appOptions.Authority);
+ }
+ else
+ {
+ jwtOptions.Authority = AuthorityHelpers.EnsureAuthorityIsV2(
+ IdentityOptionsHelpers.BuildAuthority(appOptions));
+ }
+ });
+
+ // Register core services
+ builder.Services.AddSingleton();
+ builder.Services.AddHttpContextAccessor();
+ builder.Services.AddHttpClient();
+ builder.Services.TryAddSingleton();
+ builder.Services.AddRequiredScopeAuthorization();
+ builder.Services.AddRequiredScopeOrAppPermissionAuthorization();
+
+ builder.Services.AddOptions();
+
+ // Register the post-configurator for AOT path
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, MicrosoftIdentityJwtBearerOptionsPostConfigurator>());
+
+ // Register the merger to bridge MicrosoftIdentityApplicationOptions to MergedOptions
+ // This ensures TokenAcquisition works without modification
+ if (!HasImplementationType(builder.Services, typeof(MicrosoftIdentityApplicationOptionsMerger)))
+ {
+ builder.Services.TryAddSingleton,
+ MicrosoftIdentityApplicationOptionsMerger>();
+ }
+
+ return builder;
+ }
+ }
+}
+
+#endif
diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs
index a92b9dd90..0e1d0ac3e 100644
--- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs
+++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs
@@ -14,6 +14,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
+using Microsoft.Identity.Web.Internal;
using Microsoft.Identity.Web.Resource;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Validators;
@@ -23,7 +24,7 @@ namespace Microsoft.Identity.Web
///
/// Extensions for for startup initialization of web APIs.
///
- public static class MicrosoftIdentityWebApiAuthenticationBuilderExtensions
+ public static partial class MicrosoftIdentityWebApiAuthenticationBuilderExtensions
{
///
/// Protects the web API with Microsoft identity platform (formerly Azure AD v2.0).
@@ -203,20 +204,10 @@ private static void AddMicrosoftIdentityWebApiImplementation(
mergedOptions);
}
- // If the developer registered an IssuerValidator, do not overwrite it
- if (options.TokenValidationParameters.ValidateIssuer && options.TokenValidationParameters.IssuerValidator == null)
- {
- // Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
- // we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens)
- MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory =
- serviceProvider.GetRequiredService();
-
- options.TokenValidationParameters.IssuerValidator =
- microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate;
- }
+ // Configure issuer validation
+ IdentityOptionsHelpers.ConfigureIssuerValidation(options, serviceProvider);
// If you provide a token decryption certificate, it will be used to decrypt the token
- // TODO use the credential loader
if (mergedOptions.TokenDecryptionCredentials != null)
{
DefaultCertificateLoader.UserAssignedManagedIdentityClientId = mergedOptions.UserAssignedManagedIdentityClientId;
@@ -225,17 +216,8 @@ private static void AddMicrosoftIdentityWebApiImplementation(
options.TokenValidationParameters.TokenDecryptionKeys = keys;
}
- if (options.Events == null)
- {
- options.Events = new JwtBearerEvents();
- }
-
options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();
- options.Events.OnMessageReceived += async context =>
- {
- context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
- await Task.CompletedTask.ConfigureAwait(false);
- };
+ IdentityOptionsHelpers.InitializeJwtBearerEvents(options);
// When an access token for our own web API is validated, we add it to MSAL.NET's cache so that it can
// be used from the controllers.
diff --git a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj
index cd3c7e41b..202d22346 100644
--- a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj
+++ b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj
@@ -1,7 +1,7 @@
-
+
- net9.0
+ net10.0
$(TargetFrameworks); net10.0
Exe
true
@@ -9,6 +9,7 @@
false
false
false
+ true
diff --git a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs
index eadd92b4e..1fe2d2b60 100644
--- a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs
+++ b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs
@@ -1,11 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Identity.Web;
+using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
+
internal sealed class Program
{
// The code in this program is expected to be trim and AOT compatible
private static int Main()
{
- return 100;
+ var builder = WebApplication.CreateSlimBuilder();
+
+ var azureAdSection = builder.Configuration.GetSection("AzureAd");
+ builder.Services
+ .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddMicrosoftIdentityWebApiAot(
+ options => azureAdSection.Bind(options),
+ JwtBearerDefaults.AuthenticationScheme,
+ null);
+
+ builder.Services.AddTokenAcquisition()
+ .AddInMemoryTokenCaches();
+
+ return 0;
}
}
diff --git a/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs b/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs
new file mode 100644
index 000000000..274aa1600
--- /dev/null
+++ b/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs
@@ -0,0 +1,503 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NET10_0_OR_GREATER
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.PostConfigureOptions;
+using Microsoft.Identity.Web.Resource;
+using Microsoft.Identity.Web.Test.Common;
+using Microsoft.Identity.Web.Test.Common.TestHelpers;
+using NSubstitute;
+using Xunit;
+
+namespace Microsoft.Identity.Web.Test
+{
+ public class WebApiExtensionsAotTests
+ {
+ private const string ConfigSectionName = "AzureAd";
+ private const string JwtBearerScheme = JwtBearerDefaults.AuthenticationScheme;
+
+ [Fact]
+ public void AddMicrosoftIdentityWebApiAot_WithConfigSection_RegistersServices()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+
+ // Act
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(
+ options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ },
+ JwtBearerScheme,
+ null);
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+
+ // Verify core services are registered
+ Assert.Contains(services, s => s.ServiceType == typeof(IHttpContextAccessor));
+ Assert.Contains(services, s => s.ServiceType == typeof(IMergedOptionsStore));
+ Assert.Contains(services, s => s.ServiceType == typeof(MicrosoftIdentityIssuerValidatorFactory));
+
+ // Verify MicrosoftIdentityApplicationOptions can be retrieved
+ var appOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+ Assert.NotNull(appOptions);
+
+ // Verify JWT bearer options can be retrieved
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+ Assert.NotNull(jwtOptions);
+
+ // Verify post-configurator is registered
+ var postConfigurators = services.Where(s =>
+ s.ServiceType == typeof(IPostConfigureOptions) &&
+ (s.ImplementationFactory != null || s.ImplementationType != null)).ToList();
+ Assert.NotEmpty(postConfigurators);
+ }
+
+ [Fact]
+ public void AddMicrosoftIdentityWebApiAot_WithAction_RegistersServices()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+
+ // Act
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+
+ // Verify MicrosoftIdentityApplicationOptions are configured
+ var appOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+ Assert.Equal(TestConstants.AadInstance, appOptions.Instance);
+ Assert.Equal(TestConstants.TenantIdAsGuid, appOptions.TenantId);
+ Assert.Equal(TestConstants.ClientId, appOptions.ClientId);
+
+ // Verify core services are registered
+ Assert.Contains(services, s => s.ServiceType == typeof(IHttpContextAccessor));
+ Assert.Contains(services, s => s.ServiceType == typeof(IMergedOptionsStore));
+ Assert.Contains(services, s => s.ServiceType == typeof(MicrosoftIdentityIssuerValidatorFactory));
+ }
+
+ [Fact]
+ public void AddMicrosoftIdentityWebApiAot_WithCustomJwtBearerOptions_AppliesConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+ bool customOptionsApplied = false;
+
+ // Act
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(
+ options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ },
+ JwtBearerScheme,
+ jwtOptions =>
+ {
+ customOptionsApplied = true;
+ jwtOptions.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
+ });
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ Assert.True(customOptionsApplied);
+ Assert.Equal(TimeSpan.FromMinutes(5), jwtOptions.TokenValidationParameters.ClockSkew);
+ }
+
+ [Fact]
+ public void PostConfigurator_ConfiguresAuthority()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ var provider = services.BuildServiceProvider();
+
+ // Act
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Assert
+ Assert.NotNull(jwtOptions.Authority);
+ Assert.Contains(TestConstants.TenantIdAsGuid, jwtOptions.Authority, StringComparison.Ordinal);
+ Assert.EndsWith("/v2.0", jwtOptions.Authority, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void PostConfigurator_ConfiguresAudienceValidation()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ var provider = services.BuildServiceProvider();
+
+ // Act
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Assert
+ Assert.NotNull(jwtOptions.TokenValidationParameters);
+ Assert.NotNull(jwtOptions.TokenValidationParameters.AudienceValidator);
+ }
+
+ [Fact]
+ public void PostConfigurator_ChainsOnTokenValidated_ForOboSupport()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ var provider = services.BuildServiceProvider();
+
+ // Act
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Assert
+ Assert.NotNull(jwtOptions.Events);
+ Assert.NotNull(jwtOptions.Events.OnTokenValidated);
+ }
+
+ [Fact]
+ public void PostConfigurator_RespectsCustomerAuthority()
+ {
+ // Arrange
+ var customAuthority = "https://custom.authority.com/tenant-id/v2.0";
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ // Customer configures their own authority AFTER our call
+ services.Configure(JwtBearerScheme, options =>
+ {
+ options.Authority = customAuthority;
+ });
+
+ var provider = services.BuildServiceProvider();
+
+ // Act
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Assert - our PostConfigure should respect the customer's authority
+ Assert.Equal(customAuthority, jwtOptions.Authority);
+ }
+
+ [Fact]
+ public void PostConfigurator_SkipsWhenNotConfigured()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+
+ // Add JWT bearer without using our AOT method
+ services.AddAuthentication()
+ .AddJwtBearer(JwtBearerScheme);
+
+ // Manually register the post-configurator
+ services.AddSingleton>(
+ sp => new TestOptionsMonitor(
+ new MicrosoftIdentityApplicationOptions()));
+
+ services.AddSingleton>(
+ sp => new MicrosoftIdentityJwtBearerOptionsPostConfigurator(
+ sp.GetRequiredService>(),
+ sp));
+
+ var provider = services.BuildServiceProvider();
+
+ // Act - PostConfigure should skip because ClientId is not set
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Assert - Should not throw, and authority should remain null
+ Assert.Null(jwtOptions.Authority);
+ }
+
+ [Fact]
+ public void ValidateRequiredOptions_ThrowsForMissingClientId()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ Instance = TestConstants.AadInstance,
+ TenantId = TestConstants.TenantIdAsGuid,
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options));
+
+ Assert.Contains("ClientId", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ValidateRequiredOptions_ThrowsForMissingInstance()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ ClientId = TestConstants.ClientId,
+ TenantId = TestConstants.TenantIdAsGuid,
+ Authority = "",
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options));
+
+ Assert.Contains("Instance", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ValidateRequiredOptions_ThrowsForMissingTenantId()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ ClientId = TestConstants.ClientId,
+ Instance = TestConstants.AadInstance,
+ Authority = "",
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options));
+
+ Assert.Contains("TenantId", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ValidateRequiredOptions_ThrowsForMissingDomain_B2C()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ ClientId = TestConstants.ClientId,
+ Instance = TestConstants.B2CInstance,
+ SignUpSignInPolicyId = TestConstants.B2CSignUpSignInUserFlow,
+ Authority = "",
+ };
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options));
+
+ Assert.Contains("Domain", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ValidateRequiredOptions_PassesWithValidOptions()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ ClientId = TestConstants.ClientId,
+ Instance = TestConstants.AadInstance,
+ TenantId = TestConstants.TenantIdAsGuid,
+ };
+
+ // Act & Assert - should not throw
+ MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options);
+ }
+
+ [Fact]
+ public void BuildAuthority_AAD_BuildsCorrectAuthority()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ Instance = TestConstants.AadInstance,
+ TenantId = TestConstants.TenantIdAsGuid,
+ };
+
+ // Act
+ var authority = Internal.IdentityOptionsHelpers.BuildAuthority(options);
+
+ // Assert
+ Assert.Contains(TestConstants.AadInstance.TrimEnd('/'), authority, StringComparison.Ordinal);
+ Assert.Contains(TestConstants.TenantIdAsGuid, authority, StringComparison.Ordinal);
+ Assert.EndsWith("/v2.0", authority, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void BuildAuthority_B2C_BuildsCorrectAuthority()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ Instance = TestConstants.B2CInstance,
+ Domain = TestConstants.B2CTenant,
+ SignUpSignInPolicyId = TestConstants.B2CSignUpSignInUserFlow,
+ };
+
+ // Act
+ var authority = Internal.IdentityOptionsHelpers.BuildAuthority(options);
+
+ // Assert
+ Assert.Contains(TestConstants.B2CTenant, authority, StringComparison.Ordinal);
+ Assert.Contains(TestConstants.B2CSignUpSignInUserFlow, authority, StringComparison.Ordinal);
+ Assert.EndsWith("/v2.0", authority, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task ChainTokenStorageHandler_ChainsExistingHandler()
+ {
+ // Arrange
+ bool existingHandlerCalled = false;
+ Func existingHandler = context =>
+ {
+ existingHandlerCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ var chainedHandler = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(existingHandler);
+
+ // Assert
+ Assert.NotNull(chainedHandler);
+
+ // Create a mock context
+ var httpContext = new DefaultHttpContext();
+ var tokenValidatedContext = new TokenValidatedContext(
+ httpContext,
+ new AuthenticationScheme(JwtBearerScheme, null, typeof(JwtBearerHandler)),
+ new JwtBearerOptions())
+ {
+ SecurityToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken()
+ };
+
+ // Execute the chained handler
+ await chainedHandler(tokenValidatedContext);
+
+ // Verify existing handler was called
+ Assert.True(existingHandlerCalled);
+ }
+
+ [Fact]
+ public async Task ChainTokenStorageHandler_WorksWithNullExistingHandler()
+ {
+ // Arrange & Act
+ var chainedHandler = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(null);
+
+ // Assert
+ Assert.NotNull(chainedHandler);
+
+ // Create a mock context
+ var httpContext = new DefaultHttpContext();
+ var tokenValidatedContext = new TokenValidatedContext(
+ httpContext,
+ new AuthenticationScheme(JwtBearerScheme, null, typeof(JwtBearerHandler)),
+ new JwtBearerOptions())
+ {
+ SecurityToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken()
+ };
+
+ // Execute the handler - should not throw
+ await chainedHandler(tokenValidatedContext);
+ }
+
+ [Fact]
+ public void ConfigureAudienceValidation_SetsAudienceValidator()
+ {
+ // Arrange
+ var options = new MicrosoftIdentityApplicationOptions
+ {
+ ClientId = TestConstants.ClientId,
+ Instance = TestConstants.AadInstance,
+ TenantId = TestConstants.TenantIdAsGuid,
+ };
+ var tokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters();
+
+ // Act
+ Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(
+ tokenValidationParameters,
+ options.ClientId,
+ !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId));
+
+ // Assert
+ Assert.NotNull(tokenValidationParameters.AudienceValidator);
+ }
+
+ [Fact]
+ public void AddMicrosoftIdentityWebApiAot_WithTokenAcquisition_EnablesObo()
+ {
+ // Arrange
+ var services = new ServiceCollection().AddLogging();
+
+ // Act - The key test: OBO should work without calling EnableTokenAcquisitionToCallDownstreamApi
+ services.AddAuthentication()
+ .AddMicrosoftIdentityWebApiAot(options =>
+ {
+ options.Instance = TestConstants.AadInstance;
+ options.TenantId = TestConstants.TenantIdAsGuid;
+ options.ClientId = TestConstants.ClientId;
+ }, JwtBearerScheme, null);
+
+ services.AddTokenAcquisition();
+
+ var provider = services.BuildServiceProvider();
+
+ // Assert
+ var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme);
+
+ // Verify OnTokenValidated is set up for OBO
+ Assert.NotNull(jwtOptions.Events);
+ Assert.NotNull(jwtOptions.Events.OnTokenValidated);
+
+ // Verify MergedOptions are populated via the merger
+ var mergedOptionsStore = provider.GetRequiredService();
+ var mergedOptions = mergedOptionsStore.Get(JwtBearerScheme);
+ Assert.NotNull(mergedOptions);
+ Assert.Equal(TestConstants.ClientId, mergedOptions.ClientId);
+ }
+ }
+}
+
+#endif