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..7906b9ac60
--- /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,
+ string assertion)
+ : base(confidentialClientApplicationExecutor)
+ {
+ Parameters.Username = username;
+ Parameters.Assertion = assertion;
+ }
+
+ internal static AcquireTokenByUserFederatedIdentityCredentialParameterBuilder Create(
+ IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
+ IEnumerable scopes,
+ string username,
+ string assertion)
+ {
+ if (string.IsNullOrEmpty(username))
+ {
+ throw new ArgumentNullException(nameof(username));
+ }
+
+ if (string.IsNullOrEmpty(assertion))
+ {
+ throw new ArgumentNullException(nameof(assertion));
+ }
+
+ return new AcquireTokenByUserFederatedIdentityCredentialParameterBuilder(
+ confidentialClientApplicationExecutor, username, assertion)
+ .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..2256f88e99
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenByUserFederatedIdentityCredentialParameters.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Identity.Client.Core;
+
+namespace Microsoft.Identity.Client.ApiConfig.Parameters
+{
+ internal class AcquireTokenByUserFederatedIdentityCredentialParameters : IAcquireTokenParameters
+ {
+ public string Username { get; set; }
+ public string Assertion { 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/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs
index cea2e10ccb..50d6c17568 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,
+ string assertion)
+ {
+ return AcquireTokenByUserFederatedIdentityCredentialParameterBuilder.Create(
+ ClientExecutorFactory.CreateConfidentialClientExecutor(this),
+ scopes,
+ username,
+ assertion);
+ }
+
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..8eff4a19e6
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/IByUserFederatedIdentityCredential.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+
+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.
+ ///
+ /// 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,
+ string assertion);
+ }
+}
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..f73bc3490c
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/UserFederatedIdentityCredentialRequest.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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 additionalBodyParameters = GetAdditionalBodyParameters(_userFicParameters.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 e69de29bb2..101adba535 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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 e69de29bb2..101adba535 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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 e69de29bb2..101adba535 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
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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 e69de29bb2..101adba535 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
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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 e69de29bb2..101adba535 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
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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 e69de29bb2..101adba535 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
@@ -0,0 +1,5 @@
+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.IByUserFederatedIdentityCredential
+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/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.Integration.netcore/HeadlessTests/Agentic.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs
index 3e8d7b4946..7d150e8eb5 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,18 @@ 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
+ 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, assertion)
.ExecuteAsync()
.ConfigureAwait(false);
@@ -112,29 +121,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;
- }
}
}
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..036afe33f0
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/UserFederatedIdentityCredentialTests.cs
@@ -0,0 +1,179 @@
+// 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.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.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);
+
+ var result = await (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ FakeUsername,
+ FakeAssertion)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
+ }
+
+ [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,
+ 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);
+ Assert.IsNotNull(accounts, "Accounts should not be null after token is stored in user cache.");
+ }
+
+ [TestMethod]
+ public async Task AcquireTokenByUserFic_WithForceRefresh_CallsIdentityProvider_Async()
+ {
+ using var httpManager = new MockHttpManager();
+ httpManager.AddInstanceDiscoveryMockHandler();
+
+ // 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);
+
+ var firstResult = await (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ FakeUsername,
+ FakeAssertion)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ Assert.AreEqual(TokenSource.IdentityProvider, firstResult.AuthenticationResultMetadata.TokenSource);
+
+ // Second call with ForceRefresh - should call the identity provider again
+ AddMockHandlerForUserFic(httpManager);
+
+ var secondResult = await (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ FakeUsername,
+ FakeAssertion)
+ .WithForceRefresh(true)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ Assert.AreEqual(TokenSource.IdentityProvider, secondResult.AuthenticationResultMetadata.TokenSource);
+ }
+
+ [TestMethod]
+ public void AcquireTokenByUserFic_NullUsername_ThrowsArgumentNullException()
+ {
+ using var httpManager = new MockHttpManager();
+ var app = BuildCCA(httpManager);
+
+ AssertException.Throws(() =>
+ (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ username: null,
+ assertion: FakeAssertion));
+ }
+
+ [TestMethod]
+ public void AcquireTokenByUserFic_NullAssertion_ThrowsArgumentNullException()
+ {
+ using var httpManager = new MockHttpManager();
+ var app = BuildCCA(httpManager);
+
+ AssertException.Throws(() =>
+ (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ username: FakeUsername,
+ assertion: null));
+ }
+
+ [TestMethod]
+ public void AcquireTokenByUserFic_EmptyAssertion_ThrowsArgumentNullException()
+ {
+ using var httpManager = new MockHttpManager();
+ var app = BuildCCA(httpManager);
+
+ AssertException.Throws(() =>
+ (app as IByUserFederatedIdentityCredential)
+ .AcquireTokenByUserFederatedIdentityCredential(
+ TestConstants.s_scope,
+ username: FakeUsername,
+ assertion: string.Empty));
+ }
+ }
+}