From 09ad38912ed3c9fb74a66bbe70330087775d5a43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:47:39 +0000 Subject: [PATCH 01/10] Initial plan From 31203193bf3dfe854f3bab47d8b9e879528ce9d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:02:16 +0000 Subject: [PATCH 02/10] Implement UserFIC: Add FederatedCredentialProvider, IByUserFederatedIdentityCredential interface, and UserFederatedIdentityCredentialRequest Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- ...ratedIdentityCredentialParameterBuilder.cs | 109 ++++++++ .../Executors/ConfidentialClientExecutor.cs | 23 ++ .../IConfidentialClientApplicationExecutor.cs | 5 + ...erFederatedIdentityCredentialParameters.cs | 22 ++ .../AppConfig/FederatedCredentialProvider.cs | 97 +++++++ .../ConfidentialClientApplication.cs | 16 +- .../IByUserFederatedIdentityCredential.cs | 34 +++ .../IConfidentialClientApplication.cs | 2 +- .../UserFederatedIdentityCredentialRequest.cs | 72 +++++ .../OAuth2/OAuthConstants.cs | 2 + .../PublicApi/net462/PublicAPI.Unshipped.txt | 9 +- .../PublicApi/net472/PublicAPI.Unshipped.txt | 8 + .../net8.0-android/PublicAPI.Unshipped.txt | 8 + .../net8.0-ios/PublicAPI.Unshipped.txt | 8 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 8 + .../netstandard2.0/PublicAPI.Unshipped.txt | 8 + .../TelemetryCore/Internal/Events/ApiEvent.cs | 3 + .../UserFederatedIdentityCredentialTests.cs | 248 ++++++++++++++++++ 18 files changed, 679 insertions(+), 3 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs create mode 100644 src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs create mode 100644 src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs create mode 100644 src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs create mode 100644 src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs new file mode 100644 index 0000000000..cf77390787 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Executors; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.TelemetryCore.Internal.Events; + +namespace Microsoft.Identity.Client +{ + /// + /// Parameter builder for the + /// operation. + /// +#if !SUPPORTS_CONFIDENTIAL_CLIENT + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile +#endif + public sealed class AcquireTokenByUserFederatedIdentityCredentialParameterBuilder : + AbstractConfidentialClientAcquireTokenParameterBuilder + { + private AcquireTokenByUserFederatedIdentityCredentialParameters Parameters { get; } = new AcquireTokenByUserFederatedIdentityCredentialParameters(); + + internal AcquireTokenByUserFederatedIdentityCredentialParameterBuilder( + IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, + string username, + Func> assertionProvider) + : base(confidentialClientApplicationExecutor) + { + Parameters.Username = username; + Parameters.AssertionProvider = assertionProvider; + } + + internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Create( + IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, + IEnumerable scopes, + string username, + Func> assertionProvider) + { + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentNullException(nameof(username)); + } + + if (assertionProvider == null) + { + throw new ArgumentNullException(nameof(assertionProvider)); + } + + return new AcquireTokenByUserFederatedIdentityCredentialParameterBuilder( + confidentialClientApplicationExecutor, username, assertionProvider) + .WithScopes(scopes); + } + + /// + /// Forces MSAL to refresh the token from the identity provider even if a cached token is available. + /// + /// true to bypass the cache; otherwise false. Default is false. + /// The builder to chain the .With methods + public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithForceRefresh(bool forceRefresh) + { + Parameters.ForceRefresh = forceRefresh; + return this; + } + + /// + /// Applicable to first-party applications only, this method also allows to specify + /// if the x5c claim should be sent to Azure AD. + /// Sending the x5c enables application developers to achieve easy certificate roll-over in Azure AD: + /// this method will send the certificate chain to Azure AD along with the token request, + /// so that Azure AD can use it to validate the subject name based on a trusted issuer policy. + /// This saves the application admin from the need to explicitly manage the certificate rollover + /// (either via portal or PowerShell/CLI operation). For details see https://aka.ms/msal-net-sni + /// + /// true if the x5c should be sent. Otherwise false. + /// The default is false + /// The builder to chain the .With methods + public AcquireTokenByUserFederatedIdentityCredentialParameterBuilder WithSendX5C(bool withSendX5C) + { + Parameters.SendX5C = withSendX5C; + return this; + } + + /// + internal override Task ExecuteInternalAsync(CancellationToken cancellationToken) + { + return ConfidentialClientApplicationExecutor.ExecuteAsync(CommonParameters, Parameters, cancellationToken); + } + + /// + internal override ApiEvent.ApiIds CalculateApiEventId() + { + return ApiEvent.ApiIds.AcquireTokenByUserFederatedIdentityCredential; + } + + /// + protected override void Validate() + { + base.Validate(); + + if (Parameters.SendX5C == null) + { + Parameters.SendX5C = ServiceBundle.Config?.SendX5C ?? false; + } + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs index 671870daee..8b1178cf78 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs @@ -164,5 +164,28 @@ public async Task ExecuteAsync( return await handler.RunAsync(cancellationToken).ConfigureAwait(false); } + + public async Task ExecuteAsync( + AcquireTokenCommonParameters commonParameters, + AcquireTokenByUserFederatedIdentityCredentialParameters userFicParameters, + CancellationToken cancellationToken) + { + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); + + AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + commonParameters, + requestContext, + _confidentialClientApplication.UserTokenCacheInternal, + cancellationToken).ConfigureAwait(false); + + requestParams.SendX5C = userFicParameters.SendX5C ?? false; + + var handler = new UserFederatedIdentityCredentialRequest( + ServiceBundle, + requestParams, + userFicParameters); + + return await handler.RunAsync(cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs index 60bdb24189..87a7ee4c75 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/IConfidentialClientApplicationExecutor.cs @@ -37,5 +37,10 @@ Task ExecuteAsync( AcquireTokenCommonParameters commonParameters, AcquireTokenByUsernamePasswordParameters usernamePasswordParameters, CancellationToken cancellationToken); + + Task ExecuteAsync( + AcquireTokenCommonParameters commonParameters, + AcquireTokenByUserFederatedIdentityCredentialParameters userFicParameters, + CancellationToken cancellationToken); } } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs new file mode 100644 index 0000000000..1859e9f8c8 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; + +namespace Microsoft.Identity.Client.ApiConfig.Parameters +{ + internal class AcquireTokenByUserFederatedIdentityCredentialParameters : IAcquireTokenParameters + { + public string Username { get; set; } + public Func> AssertionProvider { get; set; } + public bool? SendX5C { get; set; } + public bool ForceRefresh { get; set; } + + /// + public void LogParameters(ILoggerAdapter logger) + { + } + } +} diff --git a/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs b/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs new file mode 100644 index 0000000000..7b792063b2 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client.AppConfig; + +namespace Microsoft.Identity.Client +{ + /// + /// Factory methods to create common delegates for use with + /// . + /// +#if !SUPPORTS_CONFIDENTIAL_CLIENT + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile +#endif + public static class FederatedCredentialProvider + { + private const string DefaultAudience = "api://AzureADTokenExchange/.default"; + + /// + /// Creates an assertion provider delegate that acquires a token from a Managed Identity. + /// + /// + /// The managed identity to use. Use for system-assigned + /// or for user-assigned. + /// + /// + /// The audience (resource) for which the managed identity token is acquired. + /// Defaults to api://AzureADTokenExchange/.default. + /// + /// A delegate that acquires a managed identity token and returns its access token string. + /// Thrown if is null. + public static Func> FromManagedIdentity( + ManagedIdentityId managedIdentityId, + string audience = DefaultAudience) + { + if (managedIdentityId == null) + { + throw new ArgumentNullException(nameof(managedIdentityId)); + } + + if (audience == null) + { + throw new ArgumentNullException(nameof(audience)); + } + + // Eagerly build the ManagedIdentityApplication + var miApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build(); + + return async (options) => + { + var result = await miApp + .AcquireTokenForManagedIdentity(audience) + .ExecuteAsync(options.CancellationToken) + .ConfigureAwait(false); + + return result.AccessToken; + }; + } + + /// + /// Creates an assertion provider delegate that acquires a token from a Confidential Client Application. + /// + /// The confidential client application to use for token acquisition. + /// + /// The audience (scope) for which the confidential client acquires a token. + /// Defaults to api://AzureADTokenExchange/.default. + /// + /// A delegate that acquires a confidential client token and returns its access token string. + /// Thrown if is null. + public static Func> FromConfidentialClient( + IConfidentialClientApplication cca, + string audience = DefaultAudience) + { + if (cca == null) + { + throw new ArgumentNullException(nameof(cca)); + } + + if (audience == null) + { + throw new ArgumentNullException(nameof(audience)); + } + + return async (options) => + { + var result = await cca + .AcquireTokenForClient(new[] { audience }) + .ExecuteAsync(options.CancellationToken) + .ConfigureAwait(false); + + return result.AccessToken; + }; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index cea2e10ccb..2c6d809571 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -25,7 +25,8 @@ public sealed partial class ConfidentialClientApplication IConfidentialClientApplication, IByRefreshToken, ILongRunningWebApi, - IByUsernameAndPassword + IByUsernameAndPassword, + IByUserFederatedIdentityCredential { /// /// Instructs MSAL to try to auto discover the Azure region. @@ -183,6 +184,19 @@ AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder IByUsernameAndPass password); } + /// + AcquireTokenByUserFederatedIdentityCredentialParameterBuilder IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential( + IEnumerable scopes, + string username, + Func> assertionProvider) + { + return AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.Create( + ClientExecutorFactory.CreateConfidentialClientExecutor(this), + scopes, + username, + assertionProvider); + } + AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefreshToken( IEnumerable scopes, string refreshToken) diff --git a/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs b/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs new file mode 100644 index 0000000000..85540dd3d2 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client +{ + /// + /// Provides an interface for acquiring tokens using the User Federated Identity Credential (UserFIC) flow. + /// +#if !SUPPORTS_CONFIDENTIAL_CLIENT + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile +#endif + public interface IByUserFederatedIdentityCredential + { + /// + /// Acquires a token on behalf of a user using a federated identity credential assertion. + /// This uses the user_fic grant type. + /// + /// Scopes requested to access a protected API. + /// The UPN (User Principal Name) of the user, e.g. john.doe@contoso.com. + /// + /// A delegate that returns the federated identity credential assertion (JWT) for the user. + /// Use to create common implementations. + /// + /// A builder enabling you to add optional parameters before executing the token request. + AcquireTokenByUserFederatedIdentityCredentialParameterBuilder AcquireTokenByUserFederatedIdentityCredential( + IEnumerable scopes, + string username, + Func> assertionProvider); + } +} diff --git a/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs index 90f6e7791d..8eced85a84 100644 --- a/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/IConfidentialClientApplication.cs @@ -21,7 +21,7 @@ namespace Microsoft.Identity.Client #if !SUPPORTS_CONFIDENTIAL_CLIENT [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile #endif - public partial interface IConfidentialClientApplication : IClientApplicationBase + public partial interface IConfidentialClientApplication : IClientApplicationBase, IByUserFederatedIdentityCredential { /// /// Application token cache which holds access tokens for this application. It's maintained diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs new file mode 100644 index 0000000000..a9f08809bb --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client.Internal.Requests +{ + internal class UserFederatedIdentityCredentialRequest : RequestBase + { + private readonly AcquireTokenByUserFederatedIdentityCredentialParameters _userFicParameters; + + public UserFederatedIdentityCredentialRequest( + IServiceBundle serviceBundle, + AuthenticationRequestParameters authenticationRequestParameters, + AcquireTokenByUserFederatedIdentityCredentialParameters userFicParameters) + : base(serviceBundle, authenticationRequestParameters, userFicParameters) + { + _userFicParameters = userFicParameters; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + await ResolveAuthorityAsync().ConfigureAwait(false); + + var assertionRequestOptions = new AssertionRequestOptions + { + CancellationToken = cancellationToken + }; + + string assertion = await _userFicParameters.AssertionProvider(assertionRequestOptions).ConfigureAwait(false); + + var additionalBodyParameters = GetAdditionalBodyParameters(assertion); + MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(additionalBodyParameters, cancellationToken).ConfigureAwait(false); + + return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false); + } + + private Dictionary GetAdditionalBodyParameters(string assertion) + { + var dict = new Dictionary + { + [OAuth2Parameter.GrantType] = OAuth2GrantType.UserFic, + [OAuth2Parameter.Username] = _userFicParameters.Username, + [OAuth2Parameter.UserFederatedIdentityCredential] = assertion + }; + + ISet unionScope = new HashSet + { + OAuth2Value.ScopeOpenId, + OAuth2Value.ScopeOfflineAccess, + OAuth2Value.ScopeProfile + }; + + unionScope.UnionWith(AuthenticationRequestParameters.Scope); + dict[OAuth2Parameter.Scope] = unionScope.AsSingleString(); + dict[OAuth2Parameter.ClientInfo] = "1"; + + return dict; + } + + protected override KeyValuePair? GetCcsHeader(IDictionary additionalBodyParameters) + { + return GetCcsUpnHeader(_userFicParameters.Username); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs index 3ef7a55a6b..9a15c34ad8 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs @@ -46,6 +46,7 @@ internal static class OAuth2Parameter public const string SpaCode = "return_spa_code"; // not a standard OAuth2 param public const string FmiPath = "fmi_path"; // not a standard OAuth2 param public const string Attributes = "attributes"; // not a standard OAuth2 param + public const string UserFederatedIdentityCredential = "user_federated_identity_credential"; // user_fic grant type parameter } internal static class OAuth2GrantType @@ -58,6 +59,7 @@ internal static class OAuth2GrantType public const string JwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"; public const string Password = "password"; public const string DeviceCode = "device_code"; + public const string UserFic = "user_fic"; } internal static class OAuth2ResponseType diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 5f1ab1006d..6e1ca838ad 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ -Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken \ No newline at end of file +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationTokenMicrosoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 49f0d092b6..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 49f0d092b6..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 49f0d092b6..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 49f0d092b6..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 49f0d092b6..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.FederatedCredentialProvider +static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> +Microsoft.Identity.Client.IByUserFederatedIdentityCredential +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs index ade197d99f..d6ecf2a84c 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs @@ -39,6 +39,9 @@ public enum ApiIds InitiateLongRunningObo = 1017, AcquireTokenInLongRunningObo = 1018, + // UserFIC + AcquireTokenByUserFederatedIdentityCredential = 1019, + // "2002" is reserved for 1p OTEL signal that telemetry is disabled } diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs new file mode 100644 index 0000000000..6de255c2a7 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Test.Common; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit.RequestsTests +{ + [TestClass] + public class UserFederatedIdentityCredentialTests : TestBase + { + private const string FakeAssertion = "fake.assertion.jwt"; + private const string FakeUsername = "user@contoso.com"; + + private MockHttpMessageHandler AddMockHandlerForUserFic( + MockHttpManager httpManager, + string authority = TestConstants.AuthorityCommonTenant) + { + var handler = new MockHttpMessageHandler + { + ExpectedUrl = authority + "oauth2/v2.0/token", + ExpectedMethod = HttpMethod.Post, + ExpectedPostData = new Dictionary + { + { OAuth2Parameter.GrantType, OAuth2GrantType.UserFic }, + { OAuth2Parameter.Username, FakeUsername }, + { OAuth2Parameter.UserFederatedIdentityCredential, FakeAssertion } + }, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessage() + }; + + httpManager.AddMockHandler(handler); + return handler; + } + + private ConfidentialClientApplication BuildCCA(MockHttpManager httpManager) + { + return ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithHttpManager(httpManager) + .BuildConcrete(); + } + + [TestMethod] + public async Task AcquireTokenByUserFic_SendsCorrectOAuth2Parameters_Async() + { + using var httpManager = new MockHttpManager(); + httpManager.AddInstanceDiscoveryMockHandler(); + AddMockHandlerForUserFic(httpManager); + + var app = BuildCCA(httpManager); + + bool assertionProviderCalled = false; + var result = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + FakeUsername, + async (options) => + { + assertionProviderCalled = true; + await Task.Yield(); + return FakeAssertion; + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.IsTrue(assertionProviderCalled, "AssertionProvider delegate should have been invoked."); + } + + [TestMethod] + public async Task AcquireTokenByUserFic_TokenIsStoredInUserCache_Async() + { + using var httpManager = new MockHttpManager(); + httpManager.AddInstanceDiscoveryMockHandler(); + AddMockHandlerForUserFic(httpManager); + + var app = BuildCCA(httpManager); + + var result = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + FakeUsername, + (options) => Task.FromResult(FakeAssertion)) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.Account, "Account should be returned from the user cache."); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + + // Token should be stored in user token cache + var accounts = await app.GetAccountsAsync().ConfigureAwait(false); + var account = accounts.GetEnumerator().Current; + } + + [TestMethod] + public async Task AcquireTokenByUserFic_WithForceRefresh_InvokesAssertionProvider_Async() + { + using var httpManager = new MockHttpManager(); + httpManager.AddInstanceDiscoveryMockHandler(); + + // First call + AddMockHandlerForUserFic(httpManager); + var app = BuildCCA(httpManager); + + int assertionCallCount = 0; + Task assertionProvider(AssertionRequestOptions options) + { + assertionCallCount++; + return Task.FromResult(FakeAssertion); + } + + var firstResult = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + FakeUsername, + assertionProvider) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(1, assertionCallCount); + Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); + + // Second call with ForceRefresh - should re-invoke the assertion provider + AddMockHandlerForUserFic(httpManager); + + var secondResult = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + FakeUsername, + assertionProvider) + .WithForceRefresh(true) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(2, assertionCallCount, "AssertionProvider should be called again with ForceRefresh."); + Assert.AreEqual(TokenSource.IdentityProvider, secondResult.AuthenticationResultMetadata.TokenSource); + } + + [TestMethod] + public async Task AcquireTokenByUserFic_PassesCancellationTokenToAssertionProvider_Async() + { + using var httpManager = new MockHttpManager(); + httpManager.AddInstanceDiscoveryMockHandler(); + AddMockHandlerForUserFic(httpManager); + + var app = BuildCCA(httpManager); + + CancellationToken capturedToken = default; + using var cts = new CancellationTokenSource(); + + var result = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + FakeUsername, + (options) => + { + capturedToken = options.CancellationToken; + return Task.FromResult(FakeAssertion); + }) + .ExecuteAsync(cts.Token) + .ConfigureAwait(false); + + Assert.IsNotNull(result); + // CancellationToken should be propagated to the assertion options + Assert.AreEqual(cts.Token, capturedToken); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException() + { + using var httpManager = new MockHttpManager(); + var app = BuildCCA(httpManager); + + _ = (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + username: null, + assertionProvider: (options) => Task.FromResult(FakeAssertion)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AcquireTokenByUserFic_NullAssertionProvider_ThrowsArgumentNullException() + { + using var httpManager = new MockHttpManager(); + var app = BuildCCA(httpManager); + + _ = (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + username: FakeUsername, + assertionProvider: null); + } + + [TestMethod] + public void FederatedCredentialProvider_FromConfidentialClient_NullCca_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => FederatedCredentialProvider.FromConfidentialClient(null)); + } + + [TestMethod] + public void FederatedCredentialProvider_FromManagedIdentity_NullId_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => FederatedCredentialProvider.FromManagedIdentity(null)); + } + + [TestMethod] + public void FederatedCredentialProvider_FromConfidentialClient_ReturnsDelegate() + { + using var httpManager = new MockHttpManager(); + var cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .Build(); + + var provider = FederatedCredentialProvider.FromConfidentialClient(cca); + + Assert.IsNotNull(provider); + } + + [TestMethod] + public void FederatedCredentialProvider_FromManagedIdentity_ReturnsDelegate() + { + var provider = FederatedCredentialProvider.FromManagedIdentity(ManagedIdentityId.SystemAssigned); + + Assert.IsNotNull(provider); + } + } +} From 08403bd7941e55b27a5b9ba4f9450b2ad4002a26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:07:00 +0000 Subject: [PATCH 03/10] Fix missing newline in PublicAPI.Unshipped.txt and unused variable in test Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- .../PublicApi/net462/PublicAPI.Unshipped.txt | 3 ++- .../RequestsTests/UserFederatedIdentityCredentialTests.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 6e1ca838ad..b9be0d189f 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ -Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationTokenMicrosoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.get -> System.Threading.CancellationToken +Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.FederatedCredentialProvider diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs index 6de255c2a7..3de87f7492 100644 --- a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs @@ -104,7 +104,7 @@ public async Task AcquireTokenByUserFic_TokenIsStoredInUserCache_Async() // Token should be stored in user token cache var accounts = await app.GetAccountsAsync().ConfigureAwait(false); - var account = accounts.GetEnumerator().Current; + Assert.IsNotNull(accounts, "Accounts should not be null after token is stored in user cache."); } [TestMethod] From 7654c5ea697e0e84bd93e2dd5ca640c2c9480aec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:03:18 +0000 Subject: [PATCH 04/10] Add integration tests for UserFIC flow using ficuser@msidlabtse4.onmicrosoft.com Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- ...ratedIdentityCredentialIntegrationTests.cs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs new file mode 100644 index 0000000000..5d40d49113 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Test.Integration.Infrastructure; +using Microsoft.Identity.Test.LabInfrastructure; +using Microsoft.Identity.Test.Unit; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Integration.HeadlessTests +{ + /// + /// Integration tests for the User Federated Identity Credential (UserFIC) flow. + /// Uses confidential client app ID 979a25aa-0daf-41a5-bcad-cebec5c7c254 and user ficuser@msidlabtse4.onmicrosoft.com. + /// The same app ID is used to both acquire the assertion token and the final user token. + /// + [TestClass] + public class UserFederatedIdentityCredentialIntegrationTests + { + private const string ClientId = "979a25aa-0daf-41a5-bcad-cebec5c7c254"; + private const string Tenant = "msidlabtse4.onmicrosoft.com"; + private const string Authority = "https://login.microsoftonline.com/" + Tenant; + private const string UserUpn = "ficuser@msidlabtse4.onmicrosoft.com"; + private static readonly string[] s_scopes = { "User.Read" }; + private const string TokenExchangeAudience = "api://AzureADTokenExchange/.default"; + + [TestInitialize] + public void TestInitialize() + { + ApplicationBase.ResetStateForTest(); + } + + /// + /// Tests that the initial UserFIC token acquisition goes to the identity provider. + /// + [TestMethod] + [RunOn(TargetFrameworks.NetCore)] + public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + // Assertion app: same app ID, used to acquire the user_fic assertion + var assertionApp = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + // Main app: same app ID, acquires the final user token via user_fic grant + var app = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + + var result = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.IsNotNull(result.Account); + Assert.IsTrue( + string.Equals(UserUpn, result.Account.Username, System.StringComparison.OrdinalIgnoreCase), + $"Expected username '{UserUpn}' but got '{result.Account.Username}'"); + } + + /// + /// Tests that after initial acquisition, AcquireTokenSilent returns a cached token. + /// + [TestMethod] + [RunOn(TargetFrameworks.NetCore)] + public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + var assertionApp = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + var app = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + + // Step 1: Acquire token from identity provider + var firstResult = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(firstResult); + Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); + + // Step 2: Acquire token silently - should come from cache + var account = firstResult.Account; + var silentResult = await app + .AcquireTokenSilent(s_scopes, account) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(silentResult); + Assert.AreEqual(TokenSource.Cache, silentResult.AuthenticationResultMetadata.TokenSource, + "Second call should hit the user token cache."); + Assert.AreEqual(firstResult.AccessToken, silentResult.AccessToken); + } + + /// + /// Tests that WithForceRefresh(true) re-acquires from the identity provider even when cache is populated. + /// + [TestMethod] + [RunOn(TargetFrameworks.NetCore)] + public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() + { + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + + var assertionApp = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + var app = ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(Authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + + // Step 1: Initial acquisition from identity provider + var firstResult = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); + + // Step 2: Force refresh - should go to identity provider again + var forceRefreshResult = await (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .WithForceRefresh(true) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(forceRefreshResult); + Assert.AreEqual(TokenSource.IdentityProvider, forceRefreshResult.AuthenticationResultMetadata.TokenSource, + "WithForceRefresh(true) should bypass the cache and call the identity provider."); + } + } +} From 50ef8b964a645caddd23ae5afd02f10d184687e6 Mon Sep 17 00:00:00 2001 From: Neha Bhargava Date: Wed, 4 Mar 2026 13:30:00 -0800 Subject: [PATCH 05/10] Update integration tests to pick up the user and tenant from KV --- ...ratedIdentityCredentialIntegrationTests.cs | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs index 5d40d49113..d0e9becadb 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs @@ -14,19 +14,28 @@ namespace Microsoft.Identity.Test.Integration.HeadlessTests { /// /// Integration tests for the User Federated Identity Credential (UserFIC) flow. - /// Uses confidential client app ID 979a25aa-0daf-41a5-bcad-cebec5c7c254 and user ficuser@msidlabtse4.onmicrosoft.com. /// The same app ID is used to both acquire the assertion token and the final user token. + /// Tenant and user UPN are retrieved from Key Vault secrets at class initialization. /// [TestClass] public class UserFederatedIdentityCredentialIntegrationTests { private const string ClientId = "979a25aa-0daf-41a5-bcad-cebec5c7c254"; - private const string Tenant = "msidlabtse4.onmicrosoft.com"; - private const string Authority = "https://login.microsoftonline.com/" + Tenant; - private const string UserUpn = "ficuser@msidlabtse4.onmicrosoft.com"; private static readonly string[] s_scopes = { "User.Read" }; private const string TokenExchangeAudience = "api://AzureADTokenExchange/.default"; + private static string s_tenant; + private static string s_authority; + private static string s_userUpn; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + s_tenant = LabResponseHelper.FetchSecretString("TSETenantDomain", LabResponseHelper.KeyVaultSecretsProviderMsid); + s_userUpn = LabResponseHelper.FetchSecretString("FicUserUsername", LabResponseHelper.KeyVaultSecretsProviderMsid); + s_authority = "https://login.microsoftonline.com/" + s_tenant; + } + [TestInitialize] public void TestInitialize() { @@ -45,23 +54,23 @@ public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() // Assertion app: same app ID, used to acquire the user_fic assertion var assertionApp = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) - .WithCertificate(cert, sendX5C: true) + .WithAuthority(s_authority) + .WithCertificate(cert) .WithTestLogging() .Build(); // Main app: same app ID, acquires the final user token via user_fic grant var app = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) - .WithCertificate(cert, sendX5C: true) + .WithAuthority(s_authority) + .WithCertificate(cert) .WithTestLogging() .Build(); var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); var result = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) .ExecuteAsync() .ConfigureAwait(false); @@ -70,8 +79,8 @@ public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); Assert.IsNotNull(result.Account); Assert.IsTrue( - string.Equals(UserUpn, result.Account.Username, System.StringComparison.OrdinalIgnoreCase), - $"Expected username '{UserUpn}' but got '{result.Account.Username}'"); + string.Equals(s_userUpn, result.Account.Username, System.StringComparison.OrdinalIgnoreCase), + $"Expected username '{s_userUpn}' but got '{result.Account.Username}'"); } /// @@ -85,14 +94,14 @@ public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() var assertionApp = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) + .WithAuthority(s_authority) .WithCertificate(cert, sendX5C: true) .WithTestLogging() .Build(); var app = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) + .WithAuthority(s_authority) .WithCertificate(cert, sendX5C: true) .WithTestLogging() .Build(); @@ -101,7 +110,7 @@ public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() // Step 1: Acquire token from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) .ExecuteAsync() .ConfigureAwait(false); @@ -132,14 +141,14 @@ public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() var assertionApp = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) + .WithAuthority(s_authority) .WithCertificate(cert, sendX5C: true) .WithTestLogging() .Build(); var app = ConfidentialClientApplicationBuilder .Create(ClientId) - .WithAuthority(Authority) + .WithAuthority(s_authority) .WithCertificate(cert, sendX5C: true) .WithTestLogging() .Build(); @@ -148,7 +157,7 @@ public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() // Step 1: Initial acquisition from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) .ExecuteAsync() .ConfigureAwait(false); @@ -156,7 +165,7 @@ public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() // Step 2: Force refresh - should go to identity provider again var forceRefreshResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, UserUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) .WithForceRefresh(true) .ExecuteAsync() .ConfigureAwait(false); From 2352c7e940fffcb83c59dac01c4927c2941267fe Mon Sep 17 00:00:00 2001 From: Neha Bhargava Date: Wed, 4 Mar 2026 14:51:00 -0800 Subject: [PATCH 06/10] Update the agentic tests to use user fic --- .../HeadlessTests/Agentic.cs | 72 ++++++++----------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs index 2c2fd268fa..4067b0d7aa 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs @@ -54,6 +54,21 @@ private static async Task AgentGetsAppTokenForGraph() private static async Task AgentUserIdentityGetsTokenForGraphAsync() { + // Assertion app: acquires the user_fic assertion via FMI path + var assertionApp = ConfidentialClientApplicationBuilder + .Create(AgentIdentity) + .WithAuthority("https://login.microsoftonline.com/", TenantId) + .WithExperimentalFeatures(true) + .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) + .WithClientAssertion(async (AssertionRequestOptions a) => + { + Assert.AreEqual(AgentIdentity, a.ClientAssertionFmiPath); + var cred = await GetAppCredentialAsync(a.ClientAssertionFmiPath).ConfigureAwait(false); + return cred; + }) + .Build(); + + // Main app: acquires the final user token via user_fic grant var cca = ConfidentialClientApplicationBuilder .Create(AgentIdentity) .WithAuthority("https://login.microsoftonline.com/", TenantId) @@ -63,24 +78,21 @@ private static async Task AgentUserIdentityGetsTokenForGraphAsync() .WithClientAssertion((AssertionRequestOptions _) => GetAppCredentialAsync(AgentIdentity)) .Build(); - var result = await (cca as IByUsernameAndPassword).AcquireTokenByUsernamePassword([Scope], UserUpn, "no_password") - .OnBeforeTokenRequest( - async (request) => - { - string userFicAssertion = await GetUserFic().ConfigureAwait(false); - request.BodyParameters["user_federated_identity_credential"] = userFicAssertion; - request.BodyParameters["grant_type"] = "user_fic"; - - // remove the password - request.BodyParameters.Remove("password"); - - if (request.BodyParameters.TryGetValue("client_secret", out var secret) - && secret.Equals("default", StringComparison.OrdinalIgnoreCase)) - { - request.BodyParameters.Remove("client_secret"); - } - } - ) + // Assertion provider using the assertion app with FMI path + Func> assertionProvider = async (options) => + { + var result = await assertionApp + .AcquireTokenForClient([TokenExchangeUrl]) + .WithFmiPathForClientAssertion(AgentIdentity) + .ExecuteAsync(options.CancellationToken) + .ConfigureAwait(false); + + Trace.WriteLine($"User FIC credential from : {result.AuthenticationResultMetadata.TokenSource}"); + return result.AccessToken; + }; + + var result = await (cca as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential([Scope], UserUpn, assertionProvider) .ExecuteAsync() .ConfigureAwait(false); @@ -112,29 +124,5 @@ private static async Task GetAppCredentialAsync(string fmiPath) return result.AccessToken; } - - private static async Task GetUserFic() - { - var cca1 = ConfidentialClientApplicationBuilder - .Create(AgentIdentity) - .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExperimentalFeatures(true) - .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) - .WithClientAssertion(async (AssertionRequestOptions a) => - { - Assert.AreEqual(AgentIdentity, a.ClientAssertionFmiPath); - var cred = await GetAppCredentialAsync(a.ClientAssertionFmiPath).ConfigureAwait(false); - return cred; - }) - .Build(); - - var result = await cca1.AcquireTokenForClient([TokenExchangeUrl]) - .WithFmiPathForClientAssertion(AgentIdentity) - .ExecuteAsync().ConfigureAwait(false); - - Trace.WriteLine($"User FIC credential from : {result.AuthenticationResultMetadata.TokenSource}"); - - return result.AccessToken; - } } } From 1a96c3258496794f45b1d882b820e2228e9621e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:27:19 +0000 Subject: [PATCH 07/10] Use single app instance for both assertion source and token acquisition in UserFIC integration tests Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- ...ratedIdentityCredentialIntegrationTests.cs | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs index d0e9becadb..6b77163f21 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs @@ -51,23 +51,15 @@ public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - // Assertion app: same app ID, used to acquire the user_fic assertion - var assertionApp = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert) - .WithTestLogging() - .Build(); - - // Main app: same app ID, acquires the final user token via user_fic grant + // Single app used for both the assertion source and the user token acquisition var app = ConfidentialClientApplicationBuilder .Create(ClientId) .WithAuthority(s_authority) - .WithCertificate(cert) + .WithCertificate(cert, sendX5C: true) .WithTestLogging() .Build(); - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); var result = await (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) @@ -92,13 +84,7 @@ public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - var assertionApp = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - + // Single app used for both the assertion source and the user token acquisition var app = ConfidentialClientApplicationBuilder .Create(ClientId) .WithAuthority(s_authority) @@ -106,7 +92,7 @@ public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() .WithTestLogging() .Build(); - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); // Step 1: Acquire token from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) @@ -139,13 +125,7 @@ public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - var assertionApp = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - + // Single app used for both the assertion source and the user token acquisition var app = ConfidentialClientApplicationBuilder .Create(ClientId) .WithAuthority(s_authority) @@ -153,7 +133,7 @@ public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() .WithTestLogging() .Build(); - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(assertionApp, TokenExchangeAudience); + var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); // Step 1: Initial acquisition from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) From 6093243dbb266101fa6bfe8c7581bc42b7319859 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:01:16 +0000 Subject: [PATCH 08/10] Remove FederatedCredentialProvider, simplify UserFIC API to accept plain string assertion Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- ...ratedIdentityCredentialParameterBuilder.cs | 14 +-- ...erFederatedIdentityCredentialParameters.cs | 4 +- .../AppConfig/FederatedCredentialProvider.cs | 97 ---------------- .../ConfidentialClientApplication.cs | 4 +- .../IByUserFederatedIdentityCredential.cs | 10 +- .../UserFederatedIdentityCredentialRequest.cs | 10 +- .../PublicApi/net462/PublicAPI.Unshipped.txt | 5 +- .../PublicApi/net472/PublicAPI.Unshipped.txt | 5 +- .../net8.0-android/PublicAPI.Unshipped.txt | 5 +- .../net8.0-ios/PublicAPI.Unshipped.txt | 5 +- .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 5 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 5 +- .../HeadlessTests/Agentic.cs | 21 ++-- ...ratedIdentityCredentialIntegrationTests.cs | 67 +++++------ .../UserFederatedIdentityCredentialTests.cs | 105 +++--------------- 15 files changed, 82 insertions(+), 280 deletions(-) delete mode 100644 src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs index cf77390787..7906b9ac60 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.cs @@ -12,7 +12,7 @@ namespace Microsoft.Identity.Client { /// - /// Parameter builder for the + /// Parameter builder for the /// operation. /// #if !SUPPORTS_CONFIDENTIAL_CLIENT @@ -26,31 +26,31 @@ public sealed class AcquireTokenByUserFederatedIdentityCredentialParameterBuilde internal AcquireTokenByUserFederatedIdentityCredentialParameterBuilder( IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, string username, - Func> assertionProvider) + string assertion) : base(confidentialClientApplicationExecutor) { Parameters.Username = username; - Parameters.AssertionProvider = assertionProvider; + Parameters.Assertion = assertion; } internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Create( IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor, IEnumerable scopes, string username, - Func> assertionProvider) + string assertion) { if (string.IsNullOrEmpty(username)) { throw new ArgumentNullException(nameof(username)); } - if (assertionProvider == null) + if (string.IsNullOrEmpty(assertion)) { - throw new ArgumentNullException(nameof(assertionProvider)); + throw new ArgumentNullException(nameof(assertion)); } return new AcquireTokenByUserFederatedIdentityCredentialParameterBuilder( - confidentialClientApplicationExecutor, username, assertionProvider) + confidentialClientApplicationExecutor, username, assertion) .WithScopes(scopes); } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs index 1859e9f8c8..2256f88e99 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Threading.Tasks; using Microsoft.Identity.Client.Core; namespace Microsoft.Identity.Client.ApiConfig.Parameters @@ -10,7 +8,7 @@ namespace Microsoft.Identity.Client.ApiConfig.Parameters internal class AcquireTokenByUserFederatedIdentityCredentialParameters : IAcquireTokenParameters { public string Username { get; set; } - public Func> AssertionProvider { get; set; } + public string Assertion { get; set; } public bool? SendX5C { get; set; } public bool ForceRefresh { get; set; } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs b/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs deleted file mode 100644 index 7b792063b2..0000000000 --- a/src/client/Microsoft.Identity.Client/AppConfig/FederatedCredentialProvider.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading.Tasks; -using Microsoft.Identity.Client.AppConfig; - -namespace Microsoft.Identity.Client -{ - /// - /// Factory methods to create common delegates for use with - /// . - /// -#if !SUPPORTS_CONFIDENTIAL_CLIENT - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile -#endif - public static class FederatedCredentialProvider - { - private const string DefaultAudience = "api://AzureADTokenExchange/.default"; - - /// - /// Creates an assertion provider delegate that acquires a token from a Managed Identity. - /// - /// - /// The managed identity to use. Use for system-assigned - /// or for user-assigned. - /// - /// - /// The audience (resource) for which the managed identity token is acquired. - /// Defaults to api://AzureADTokenExchange/.default. - /// - /// A delegate that acquires a managed identity token and returns its access token string. - /// Thrown if is null. - public static Func> FromManagedIdentity( - ManagedIdentityId managedIdentityId, - string audience = DefaultAudience) - { - if (managedIdentityId == null) - { - throw new ArgumentNullException(nameof(managedIdentityId)); - } - - if (audience == null) - { - throw new ArgumentNullException(nameof(audience)); - } - - // Eagerly build the ManagedIdentityApplication - var miApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build(); - - return async (options) => - { - var result = await miApp - .AcquireTokenForManagedIdentity(audience) - .ExecuteAsync(options.CancellationToken) - .ConfigureAwait(false); - - return result.AccessToken; - }; - } - - /// - /// Creates an assertion provider delegate that acquires a token from a Confidential Client Application. - /// - /// The confidential client application to use for token acquisition. - /// - /// The audience (scope) for which the confidential client acquires a token. - /// Defaults to api://AzureADTokenExchange/.default. - /// - /// A delegate that acquires a confidential client token and returns its access token string. - /// Thrown if is null. - public static Func> FromConfidentialClient( - IConfidentialClientApplication cca, - string audience = DefaultAudience) - { - if (cca == null) - { - throw new ArgumentNullException(nameof(cca)); - } - - if (audience == null) - { - throw new ArgumentNullException(nameof(audience)); - } - - return async (options) => - { - var result = await cca - .AcquireTokenForClient(new[] { audience }) - .ExecuteAsync(options.CancellationToken) - .ConfigureAwait(false); - - return result.AccessToken; - }; - } - } -} diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index 2c6d809571..50d6c17568 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -188,13 +188,13 @@ AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder IByUsernameAndPass AcquireTokenByUserFederatedIdentityCredentialParameterBuilder IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential( IEnumerable scopes, string username, - Func> assertionProvider) + string assertion) { return AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.Create( ClientExecutorFactory.CreateConfidentialClientExecutor(this), scopes, username, - assertionProvider); + assertion); } AcquireTokenByRefreshTokenParameterBuilder IByRefreshToken.AcquireTokenByRefreshToken( diff --git a/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs b/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs index 85540dd3d2..8eff4a19e6 100644 --- a/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs +++ b/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace Microsoft.Identity.Client { @@ -21,14 +19,14 @@ public interface IByUserFederatedIdentityCredential /// /// Scopes requested to access a protected API. /// The UPN (User Principal Name) of the user, e.g. john.doe@contoso.com. - /// - /// A delegate that returns the federated identity credential assertion (JWT) for the user. - /// Use to create common implementations. + /// + /// The federated identity credential assertion (JWT) for the user. + /// Acquire this token from a Managed Identity or Confidential Client application before calling this method. /// /// A builder enabling you to add optional parameters before executing the token request. AcquireTokenByUserFederatedIdentityCredentialParameterBuilder AcquireTokenByUserFederatedIdentityCredential( IEnumerable scopes, string username, - Func> assertionProvider); + string assertion); } } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs index a9f08809bb..f73bc3490c 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -28,14 +27,7 @@ protected override async Task ExecuteAsync(CancellationTok { await ResolveAuthorityAsync().ConfigureAwait(false); - var assertionRequestOptions = new AssertionRequestOptions - { - CancellationToken = cancellationToken - }; - - string assertion = await _userFicParameters.AssertionProvider(assertionRequestOptions).ConfigureAwait(false); - - var additionalBodyParameters = GetAdditionalBodyParameters(assertion); + var additionalBodyParameters = GetAdditionalBodyParameters(_userFicParameters.Assertion); MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(additionalBodyParameters, cancellationToken).ConfigureAwait(false); return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index b9be0d189f..60257557b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -2,8 +2,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.cancellationToken.g Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithForceRefresh(bool forceRefresh) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.WithSendX5C(bool withSendX5C) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder -Microsoft.Identity.Client.FederatedCredentialProvider -static Microsoft.Identity.Client.FederatedCredentialProvider.FromConfidentialClient(Microsoft.Identity.Client.IConfidentialClientApplication cca, string audience = "api://AzureADTokenExchange/.default") -> System.Func> -static Microsoft.Identity.Client.FederatedCredentialProvider.FromManagedIdentity(Microsoft.Identity.Client.AppConfig.ManagedIdentityId managedIdentityId, string audience = "api://AzureADTokenExchange/.default") -> System.Func> Microsoft.Identity.Client.IByUserFederatedIdentityCredential -Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, System.Func> assertionProvider) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder +Microsoft.Identity.Client.IByUserFederatedIdentityCredential.AcquireTokenByUserFederatedIdentityCredential(System.Collections.Generic.IEnumerable scopes, string username, string assertion) -> Microsoft.Identity.Client.AcquireTokenByUserFederatedIdentityCredentialParameterBuilder diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs index 4067b0d7aa..3fc3fbd472 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs @@ -79,20 +79,17 @@ private static async Task AgentUserIdentityGetsTokenForGraphAsync() .Build(); // Assertion provider using the assertion app with FMI path - Func> assertionProvider = async (options) => - { - var result = await assertionApp - .AcquireTokenForClient([TokenExchangeUrl]) - .WithFmiPathForClientAssertion(AgentIdentity) - .ExecuteAsync(options.CancellationToken) - .ConfigureAwait(false); - - Trace.WriteLine($"User FIC credential from : {result.AuthenticationResultMetadata.TokenSource}"); - return result.AccessToken; - }; + var assertionResult = await assertionApp + .AcquireTokenForClient([TokenExchangeUrl]) + .WithFmiPathForClientAssertion(AgentIdentity) + .ExecuteAsync() + .ConfigureAwait(false); + + Trace.WriteLine($"User FIC credential from : {assertionResult.AuthenticationResultMetadata.TokenSource}"); + string assertion = assertionResult.AccessToken; var result = await (cca as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential([Scope], UserUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential([Scope], UserUpn, assertion) .ExecuteAsync() .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs index 6b77163f21..be25fd204d 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.Identity.Test.Integration.HeadlessTests /// /// Integration tests for the User Federated Identity Credential (UserFIC) flow. /// The same app ID is used to both acquire the assertion token and the final user token. + /// The developer acquires the assertion manually and passes it as a plain string. /// Tenant and user UPN are retrieved from Key Vault secrets at class initialization. /// [TestClass] @@ -22,7 +23,7 @@ public class UserFederatedIdentityCredentialIntegrationTests { private const string ClientId = "979a25aa-0daf-41a5-bcad-cebec5c7c254"; private static readonly string[] s_scopes = { "User.Read" }; - private const string TokenExchangeAudience = "api://AzureADTokenExchange/.default"; + private static readonly string[] s_tokenExchangeScopes = { "api://AzureADTokenExchange/.default" }; private static string s_tenant; private static string s_authority; @@ -42,6 +43,26 @@ public void TestInitialize() ApplicationBase.ResetStateForTest(); } + private static IConfidentialClientApplication BuildApp(X509Certificate2 cert) + { + return ConfidentialClientApplicationBuilder + .Create(ClientId) + .WithAuthority(s_authority) + .WithCertificate(cert, sendX5C: true) + .WithTestLogging() + .Build(); + } + + private static async Task AcquireAssertionAsync(IConfidentialClientApplication app) + { + var assertionResult = await app + .AcquireTokenForClient(s_tokenExchangeScopes) + .ExecuteAsync() + .ConfigureAwait(false); + + return assertionResult.AccessToken; + } + /// /// Tests that the initial UserFIC token acquisition goes to the identity provider. /// @@ -50,19 +71,12 @@ public void TestInitialize() public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + var app = BuildApp(cert); - // Single app used for both the assertion source and the user token acquisition - var app = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); + string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); var result = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) .ExecuteAsync() .ConfigureAwait(false); @@ -83,20 +97,13 @@ public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + var app = BuildApp(cert); - // Single app used for both the assertion source and the user token acquisition - var app = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); + string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); // Step 1: Acquire token from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) .ExecuteAsync() .ConfigureAwait(false); @@ -124,28 +131,22 @@ public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() { X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); + var app = BuildApp(cert); - // Single app used for both the assertion source and the user token acquisition - var app = ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - - var assertionProvider = FederatedCredentialProvider.FromConfidentialClient(app, TokenExchangeAudience); + string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); // Step 1: Initial acquisition from identity provider var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) .ExecuteAsync() .ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); - // Step 2: Force refresh - should go to identity provider again + // Step 2: Re-acquire a fresh assertion and force refresh the user token + string freshAssertion = await AcquireAssertionAsync(app).ConfigureAwait(false); var forceRefreshResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertionProvider) + .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, freshAssertion) .WithForceRefresh(true) .ExecuteAsync() .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs index 3de87f7492..d009699cff 100644 --- a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; -using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -62,23 +60,16 @@ public async Task AcquireTokenByUserFic_SendsCorrectOAuth2Parameters_Async() var app = BuildCCA(httpManager); - bool assertionProviderCalled = false; var result = await (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, FakeUsername, - async (options) => - { - assertionProviderCalled = true; - await Task.Yield(); - return FakeAssertion; - }) + FakeAssertion) .ExecuteAsync() .ConfigureAwait(false); Assert.IsNotNull(result); Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); - Assert.IsTrue(assertionProviderCalled, "AssertionProvider delegate should have been invoked."); } [TestMethod] @@ -94,7 +85,7 @@ public async Task AcquireTokenByUserFic_TokenIsStoredInUserCache_Async() .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, FakeUsername, - (options) => Task.FromResult(FakeAssertion)) + FakeAssertion) .ExecuteAsync() .ConfigureAwait(false); @@ -108,81 +99,58 @@ public async Task AcquireTokenByUserFic_TokenIsStoredInUserCache_Async() } [TestMethod] - public async Task AcquireTokenByUserFic_WithForceRefresh_InvokesAssertionProvider_Async() + public async Task AcquireTokenByUserFic_WithForceRefresh_CallsIdentityProvider_Async() { using var httpManager = new MockHttpManager(); httpManager.AddInstanceDiscoveryMockHandler(); - // First call + // Two mock handlers are added; MockHttpManager verifies both are consumed, + // confirming that two separate calls to the identity provider were made. AddMockHandlerForUserFic(httpManager); var app = BuildCCA(httpManager); - int assertionCallCount = 0; - Task assertionProvider(AssertionRequestOptions options) - { - assertionCallCount++; - return Task.FromResult(FakeAssertion); - } - var firstResult = await (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, FakeUsername, - assertionProvider) + FakeAssertion) .ExecuteAsync() .ConfigureAwait(false); - Assert.AreEqual(1, assertionCallCount); Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); - // Second call with ForceRefresh - should re-invoke the assertion provider + // Second call with ForceRefresh - should call the identity provider again AddMockHandlerForUserFic(httpManager); var secondResult = await (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, FakeUsername, - assertionProvider) + FakeAssertion) .WithForceRefresh(true) .ExecuteAsync() .ConfigureAwait(false); - Assert.AreEqual(2, assertionCallCount, "AssertionProvider should be called again with ForceRefresh."); Assert.AreEqual(TokenSource.IdentityProvider, secondResult.AuthenticationResultMetadata.TokenSource); } [TestMethod] - public async Task AcquireTokenByUserFic_PassesCancellationTokenToAssertionProvider_Async() + [ExpectedException(typeof(ArgumentNullException))] + public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); - httpManager.AddInstanceDiscoveryMockHandler(); - AddMockHandlerForUserFic(httpManager); - var app = BuildCCA(httpManager); - CancellationToken capturedToken = default; - using var cts = new CancellationTokenSource(); - - var result = await (app as IByUserFederatedIdentityCredential) + _ = (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, - FakeUsername, - (options) => - { - capturedToken = options.CancellationToken; - return Task.FromResult(FakeAssertion); - }) - .ExecuteAsync(cts.Token) - .ConfigureAwait(false); - - Assert.IsNotNull(result); - // CancellationToken should be propagated to the assertion options - Assert.AreEqual(cts.Token, capturedToken); + username: null, + assertion: FakeAssertion); } [TestMethod] [ExpectedException(typeof(ArgumentNullException))] - public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException() + public void AcquireTokenByUserFic_NullAssertion_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); var app = BuildCCA(httpManager); @@ -190,13 +158,13 @@ public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException() _ = (app as IByUserFederatedIdentityCredential) .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, - username: null, - assertionProvider: (options) => Task.FromResult(FakeAssertion)); + username: FakeUsername, + assertion: null); } [TestMethod] [ExpectedException(typeof(ArgumentNullException))] - public void AcquireTokenByUserFic_NullAssertionProvider_ThrowsArgumentNullException() + public void AcquireTokenByUserFic_EmptyAssertion_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); var app = BuildCCA(httpManager); @@ -205,44 +173,7 @@ public void AcquireTokenByUserFic_NullAssertionProvider_ThrowsArgumentNullExcept .AcquireTokenByUserFederatedIdentityCredential( TestConstants.s_scope, username: FakeUsername, - assertionProvider: null); - } - - [TestMethod] - public void FederatedCredentialProvider_FromConfidentialClient_NullCca_ThrowsArgumentNullException() - { - Assert.ThrowsException( - () => FederatedCredentialProvider.FromConfidentialClient(null)); - } - - [TestMethod] - public void FederatedCredentialProvider_FromManagedIdentity_NullId_ThrowsArgumentNullException() - { - Assert.ThrowsException( - () => FederatedCredentialProvider.FromManagedIdentity(null)); - } - - [TestMethod] - public void FederatedCredentialProvider_FromConfidentialClient_ReturnsDelegate() - { - using var httpManager = new MockHttpManager(); - var cca = ConfidentialClientApplicationBuilder - .Create(TestConstants.ClientId) - .WithClientSecret(TestConstants.ClientSecret) - .WithHttpManager(httpManager) - .Build(); - - var provider = FederatedCredentialProvider.FromConfidentialClient(cca); - - Assert.IsNotNull(provider); - } - - [TestMethod] - public void FederatedCredentialProvider_FromManagedIdentity_ReturnsDelegate() - { - var provider = FederatedCredentialProvider.FromManagedIdentity(ManagedIdentityId.SystemAssigned); - - Assert.IsNotNull(provider); + assertion: string.Empty); } } } From bd330b3b72f43b38074aafa24a95b1557a188187 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:37:14 +0000 Subject: [PATCH 09/10] Remove UserFederatedIdentityCredentialIntegrationTests.cs Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- ...ratedIdentityCredentialIntegrationTests.cs | 159 ------------------ 1 file changed, 159 deletions(-) delete mode 100644 tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs deleted file mode 100644 index be25fd204d..0000000000 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/UserFederatedIdentityCredentialIntegrationTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Microsoft.Identity.Client; -using Microsoft.Identity.Test.Common.Core.Helpers; -using Microsoft.Identity.Test.Integration.Infrastructure; -using Microsoft.Identity.Test.LabInfrastructure; -using Microsoft.Identity.Test.Unit; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Microsoft.Identity.Test.Integration.HeadlessTests -{ - /// - /// Integration tests for the User Federated Identity Credential (UserFIC) flow. - /// The same app ID is used to both acquire the assertion token and the final user token. - /// The developer acquires the assertion manually and passes it as a plain string. - /// Tenant and user UPN are retrieved from Key Vault secrets at class initialization. - /// - [TestClass] - public class UserFederatedIdentityCredentialIntegrationTests - { - private const string ClientId = "979a25aa-0daf-41a5-bcad-cebec5c7c254"; - private static readonly string[] s_scopes = { "User.Read" }; - private static readonly string[] s_tokenExchangeScopes = { "api://AzureADTokenExchange/.default" }; - - private static string s_tenant; - private static string s_authority; - private static string s_userUpn; - - [ClassInitialize] - public static void ClassInitialize(TestContext _) - { - s_tenant = LabResponseHelper.FetchSecretString("TSETenantDomain", LabResponseHelper.KeyVaultSecretsProviderMsid); - s_userUpn = LabResponseHelper.FetchSecretString("FicUserUsername", LabResponseHelper.KeyVaultSecretsProviderMsid); - s_authority = "https://login.microsoftonline.com/" + s_tenant; - } - - [TestInitialize] - public void TestInitialize() - { - ApplicationBase.ResetStateForTest(); - } - - private static IConfidentialClientApplication BuildApp(X509Certificate2 cert) - { - return ConfidentialClientApplicationBuilder - .Create(ClientId) - .WithAuthority(s_authority) - .WithCertificate(cert, sendX5C: true) - .WithTestLogging() - .Build(); - } - - private static async Task AcquireAssertionAsync(IConfidentialClientApplication app) - { - var assertionResult = await app - .AcquireTokenForClient(s_tokenExchangeScopes) - .ExecuteAsync() - .ConfigureAwait(false); - - return assertionResult.AccessToken; - } - - /// - /// Tests that the initial UserFIC token acquisition goes to the identity provider. - /// - [TestMethod] - [RunOn(TargetFrameworks.NetCore)] - public async Task UserFic_InitialAcquisition_FromIdentityProvider_Async() - { - X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - var app = BuildApp(cert); - - string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); - - var result = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(result); - Assert.IsNotNull(result.AccessToken); - Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); - Assert.IsNotNull(result.Account); - Assert.IsTrue( - string.Equals(s_userUpn, result.Account.Username, System.StringComparison.OrdinalIgnoreCase), - $"Expected username '{s_userUpn}' but got '{result.Account.Username}'"); - } - - /// - /// Tests that after initial acquisition, AcquireTokenSilent returns a cached token. - /// - [TestMethod] - [RunOn(TargetFrameworks.NetCore)] - public async Task UserFic_SilentCacheHit_ReturnsFromCache_Async() - { - X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - var app = BuildApp(cert); - - string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); - - // Step 1: Acquire token from identity provider - var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(firstResult); - Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); - - // Step 2: Acquire token silently - should come from cache - var account = firstResult.Account; - var silentResult = await app - .AcquireTokenSilent(s_scopes, account) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(silentResult); - Assert.AreEqual(TokenSource.Cache, silentResult.AuthenticationResultMetadata.TokenSource, - "Second call should hit the user token cache."); - Assert.AreEqual(firstResult.AccessToken, silentResult.AccessToken); - } - - /// - /// Tests that WithForceRefresh(true) re-acquires from the identity provider even when cache is populated. - /// - [TestMethod] - [RunOn(TargetFrameworks.NetCore)] - public async Task UserFic_ForceRefresh_AcquiresFromIdentityProvider_Async() - { - X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); - var app = BuildApp(cert); - - string assertion = await AcquireAssertionAsync(app).ConfigureAwait(false); - - // Step 1: Initial acquisition from identity provider - var firstResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, assertion) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource); - - // Step 2: Re-acquire a fresh assertion and force refresh the user token - string freshAssertion = await AcquireAssertionAsync(app).ConfigureAwait(false); - var forceRefreshResult = await (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential(s_scopes, s_userUpn, freshAssertion) - .WithForceRefresh(true) - .ExecuteAsync() - .ConfigureAwait(false); - - Assert.IsNotNull(forceRefreshResult); - Assert.AreEqual(TokenSource.IdentityProvider, forceRefreshResult.AuthenticationResultMetadata.TokenSource, - "WithForceRefresh(true) should bypass the cache and call the identity provider."); - } - } -} From f025754cc46ff94fc2916612c2b2cc663892788d Mon Sep 17 00:00:00 2001 From: Neha Bhargava Date: Wed, 18 Mar 2026 15:04:27 -0700 Subject: [PATCH 10/10] Fix compile failures --- .../UserFederatedIdentityCredentialTests.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs index d009699cff..036afe33f0 100644 --- a/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs @@ -135,45 +135,45 @@ public async Task AcquireTokenByUserFic_WithForceRefresh_CallsIdentityProvider_A } [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); var app = BuildCCA(httpManager); - _ = (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential( - TestConstants.s_scope, - username: null, - assertion: FakeAssertion); + AssertException.Throws(() => + (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + username: null, + assertion: FakeAssertion)); } [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] public void AcquireTokenByUserFic_NullAssertion_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); var app = BuildCCA(httpManager); - _ = (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential( - TestConstants.s_scope, - username: FakeUsername, - assertion: null); + AssertException.Throws(() => + (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + username: FakeUsername, + assertion: null)); } [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] public void AcquireTokenByUserFic_EmptyAssertion_ThrowsArgumentNullException() { using var httpManager = new MockHttpManager(); var app = BuildCCA(httpManager); - _ = (app as IByUserFederatedIdentityCredential) - .AcquireTokenByUserFederatedIdentityCredential( - TestConstants.s_scope, - username: FakeUsername, - assertion: string.Empty); + AssertException.Throws(() => + (app as IByUserFederatedIdentityCredential) + .AcquireTokenByUserFederatedIdentityCredential( + TestConstants.s_scope, + username: FakeUsername, + assertion: string.Empty)); } } }