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));
}
}
}