diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs index 7cbf69f999..f8d7b274e4 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs @@ -42,6 +42,13 @@ private AcquireTokenForManagedIdentityParameterBuilder WithResource(string resou { Parameters.Resource = ScopeHelper.RemoveDefaultSuffixIfPresent(resource); CommonParameters.Scopes = new string[] { Parameters.Resource }; + + if (resource.Equals("api://AzureFMITokenExchange/.default", StringComparison.OrdinalIgnoreCase)) + { + Parameters.isFmiCredentialRequest = true; + ValidateUseOfExperimentalFeature(); + } + return this; } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs index 7c471fea59..efa739490c 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs @@ -16,6 +16,8 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter public string Resource { get; set; } + public bool isFmiCredentialRequest { get; set; } + public void LogParameters(ILoggerAdapter logger) { if (logger.IsLoggingEnabled(LogLevel.Info)) diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index cb441baa80..fe9cb2c264 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -153,7 +153,7 @@ private async Task SendTokenRequestForManagedIdentityAsync await ResolveAuthorityAsync().ConfigureAwait(false); ManagedIdentityClient managedIdentityClient = - new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext); + new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext, _managedIdentityParameters); ManagedIdentityResponse managedIdentityResponse = await managedIdentityClient diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/EnvironmentVariables.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/EnvironmentVariables.cs index aed4821dae..108814e95f 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/EnvironmentVariables.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/EnvironmentVariables.cs @@ -8,6 +8,8 @@ namespace Microsoft.Identity.Client.ManagedIdentity internal class EnvironmentVariables { public static string IdentityEndpoint => Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT"); + public static string FmiServiceFabricEndpoint => Environment.GetEnvironmentVariable("APP_IDENTITY_ENDPOINT"); + public static string FmiServiceFabricApiVersion => Environment.GetEnvironmentVariable("IDENTITY_API_VERSION"); public static string IdentityHeader => Environment.GetEnvironmentVariable("IDENTITY_HEADER"); public static string PodIdentityEndpoint => Environment.GetEnvironmentVariable("AZURE_POD_IDENTITY_AUTHORITY_HOST"); public static string ImdsEndpoint => Environment.GetEnvironmentVariable("IMDS_ENDPOINT"); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index 80a45bb0da..ab50c28f6c 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -21,11 +21,11 @@ internal class ManagedIdentityClient private const string LinuxHimdsFilePath = "/opt/azcmagent/bin/himds"; private readonly AbstractManagedIdentity _identitySource; - public ManagedIdentityClient(RequestContext requestContext) + public ManagedIdentityClient(RequestContext requestContext, AcquireTokenForManagedIdentityParameters acquireTokenForManagedIdentityParameters) { using (requestContext.Logger.LogMethodDuration()) { - _identitySource = SelectManagedIdentitySource(requestContext); + _identitySource = SelectManagedIdentitySource(requestContext, acquireTokenForManagedIdentityParameters); } } @@ -35,11 +35,16 @@ internal Task SendTokenRequestForManagedIdentityAsync(A } // This method tries to create managed identity source for different sources, if none is created then defaults to IMDS. - private static AbstractManagedIdentity SelectManagedIdentitySource(RequestContext requestContext) + private static AbstractManagedIdentity SelectManagedIdentitySource(RequestContext requestContext, AcquireTokenForManagedIdentityParameters acquireTokenForManagedIdentityParameters) { + if (acquireTokenForManagedIdentityParameters.isFmiCredentialRequest) + { + return ServiceFabricManagedIdentitySource.Create(requestContext, true); + } + return GetManagedIdentitySource(requestContext.Logger) switch { - ManagedIdentitySource.ServiceFabric => ServiceFabricManagedIdentitySource.Create(requestContext), + ManagedIdentitySource.ServiceFabric => ServiceFabricManagedIdentitySource.Create(requestContext, false), ManagedIdentitySource.AppService => AppServiceManagedIdentitySource.Create(requestContext), ManagedIdentitySource.MachineLearning => MachineLearningManagedIdentitySource.Create(requestContext), ManagedIdentitySource.CloudShell => CloudShellManagedIdentitySource.Create(requestContext), diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs index a35ce1b1bf..5c887cc87b 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs @@ -16,34 +16,79 @@ internal class ServiceFabricManagedIdentitySource : AbstractManagedIdentity private const string ServiceFabricMsiApiVersion = "2019-07-01-preview"; private readonly Uri _endpoint; private readonly string _identityHeaderValue; + private readonly bool _isFmiCredentialRequest; + private static string _mitsEndpointFmiPath => "/metadata/identity/oauth2/fmi/credential"; - internal static Lazy _httpClientLazy; - - public static AbstractManagedIdentity Create(RequestContext requestContext) + public static AbstractManagedIdentity Create(RequestContext requestContext, bool isFmiCredentialRequest = false) { + Uri endpointUri; string identityEndpoint = EnvironmentVariables.IdentityEndpoint; - requestContext.Logger.Info(() => "[Managed Identity] Service fabric managed identity is available."); - - if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri)) + if (isFmiCredentialRequest) + { + VerifyFederatedEnvVariablesAreAvailable(); + requestContext.Logger.Info(() => "[Managed Identity] Service fabric federated managed identity is available."); + identityEndpoint = EnvironmentVariables.FmiServiceFabricEndpoint; + requestContext.Logger.Info(() => "[Managed Identity] Using FMI Service fabric endpoint."); + + if (!Uri.TryCreate(identityEndpoint + _mitsEndpointFmiPath, UriKind.Absolute, out endpointUri)) + { + string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "APP_IDENTITY_ENDPOINT", identityEndpoint, "FMI Service Fabric"); + + throw MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + null, + ManagedIdentitySource.ServiceFabric, + null); + } + } + else { - string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, - "IDENTITY_ENDPOINT", identityEndpoint, "Service Fabric"); - - // Use the factory to create and throw the exception - var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( - MsalError.InvalidManagedIdentityEndpoint, - errorMessage, - null, - ManagedIdentitySource.ServiceFabric, - null); - - throw exception; + requestContext.Logger.Info(() => "[Managed Identity] Service fabric managed identity is available."); + + if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out endpointUri)) + { + string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "IDENTITY_ENDPOINT", identityEndpoint, "Service Fabric"); + + throw MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + null, + ManagedIdentitySource.ServiceFabric, + null); + } } - requestContext.Logger.Verbose(() => "[Managed Identity] Creating Service Fabric managed identity. Endpoint URI: " + identityEndpoint); - - return new ServiceFabricManagedIdentitySource(requestContext, endpointUri, EnvironmentVariables.IdentityHeader); + requestContext.Logger.Verbose(() => $"[Managed Identity] Creating Service Fabric {(isFmiCredentialRequest ? "federated" : "")} managed identity. Endpoint URI: {identityEndpoint}"); + + return new ServiceFabricManagedIdentitySource(requestContext, endpointUri, EnvironmentVariables.IdentityHeader, isFmiCredentialRequest); + } + + private static void VerifyFederatedEnvVariablesAreAvailable() + { + if (string.IsNullOrEmpty(EnvironmentVariables.IdentityServerThumbprint)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityFmiInvalidEnvVariableError, + "IDENTITY_SERVER_THUMBPRINT")); + } + if (string.IsNullOrEmpty(EnvironmentVariables.FmiServiceFabricEndpoint)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityFmiInvalidEnvVariableError, + "APP_IDENTITY_ENDPOINT")); + } + if (string.IsNullOrEmpty(EnvironmentVariables.IdentityHeader)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityFmiInvalidEnvVariableError, + "IDENTITY_HEADER")); + } + if (string.IsNullOrEmpty(EnvironmentVariables.FmiServiceFabricApiVersion)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityFmiInvalidEnvVariableError, + "IDENTITY_API_VERSION", "FMI Service Fabric")); + } } internal override Func GetValidationCallback() @@ -62,11 +107,12 @@ private bool ValidateServerCertificateCallback(HttpRequestMessage message, X509C return string.Equals(certificate.GetCertHashString(), EnvironmentVariables.IdentityServerThumbprint, StringComparison.OrdinalIgnoreCase); } - private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri endpoint, string identityHeaderValue) : - base(requestContext, ManagedIdentitySource.ServiceFabric) + private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri endpoint, string identityHeaderValue, bool isFmi) : + base(requestContext, ManagedIdentitySource.ServiceFabric) { _endpoint = endpoint; _identityHeaderValue = identityHeaderValue; + _isFmiCredentialRequest = isFmi; if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned) { @@ -77,31 +123,43 @@ private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri en protected override ManagedIdentityRequest CreateRequest(string resource) { ManagedIdentityRequest request = new ManagedIdentityRequest(HttpMethod.Get, _endpoint); - request.Headers["secret"] = _identityHeaderValue; - request.QueryParameters["api-version"] = ServiceFabricMsiApiVersion; - request.QueryParameters["resource"] = resource; - - switch (_requestContext.ServiceBundle.Config.ManagedIdentityId.IdType) + if (_isFmiCredentialRequest) { - case AppConfig.ManagedIdentityIdType.ClientId: - _requestContext.Logger.Info("[Managed Identity] Adding user assigned client id to the request."); - request.QueryParameters[Constants.ManagedIdentityClientId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; - break; - - case AppConfig.ManagedIdentityIdType.ResourceId: - _requestContext.Logger.Info("[Managed Identity] Adding user assigned resource id to the request."); - request.QueryParameters[Constants.ManagedIdentityResourceId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; - break; - - case AppConfig.ManagedIdentityIdType.ObjectId: - _requestContext.Logger.Info("[Managed Identity] Adding user assigned object id to the request."); - request.QueryParameters[Constants.ManagedIdentityObjectId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; - break; + _requestContext.Logger.Info("[Managed Identity] Request is for FMI, no ids or resource will be added to the request."); + request.QueryParameters["api-version"] = EnvironmentVariables.FmiServiceFabricApiVersion; + } + else + { + request.QueryParameters["api-version"] = ServiceFabricMsiApiVersion; + request.QueryParameters["resource"] = resource; + + switch (_requestContext.ServiceBundle.Config.ManagedIdentityId.IdType) + { + case AppConfig.ManagedIdentityIdType.ClientId: + _requestContext.Logger.Info("[Managed Identity] Adding user assigned client id to the request."); + request.QueryParameters[Constants.ManagedIdentityClientId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; + break; + + case AppConfig.ManagedIdentityIdType.ResourceId: + _requestContext.Logger.Info("[Managed Identity] Adding user assigned resource id to the request."); + request.QueryParameters[Constants.ManagedIdentityResourceId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; + break; + + case AppConfig.ManagedIdentityIdType.ObjectId: + _requestContext.Logger.Info("[Managed Identity] Adding user assigned object id to the request."); + request.QueryParameters[Constants.ManagedIdentityObjectId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId; + break; + } } return request; } + + internal string GetEndpointForTesting() + { + return _endpoint.ToString(); + } } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index b1895118aa..c93f6f5726 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -421,6 +421,7 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string ManagedIdentityUnexpectedErrorResponse = "[Managed Identity] The error response was either empty or could not be parsed."; public const string ManagedIdentityEndpointInvalidUriError = "[Managed Identity] The environment variable {0} contains an invalid Uri {1} in {2} managed identity source."; + public const string ManagedIdentityFmiInvalidEnvVariableError = "[Managed Identity] The environment variable {0} is null or empty in {1} managed identity source."; public const string ManagedIdentityNoChallengeError = "[Managed Identity] Did not receive expected WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint."; public const string ManagedIdentityInvalidChallenge = "[Managed Identity] The WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint did not match the expected format."; public const string ManagedIdentityInvalidFile = "[Managed Identity] The file on the file path in the WWW-Authenticate header is not secure."; diff --git a/tests/Microsoft.Identity.Test.Common/Core/Helpers/ManagedIdentityTestUtil.cs b/tests/Microsoft.Identity.Test.Common/Core/Helpers/ManagedIdentityTestUtil.cs index c6e0627d0c..3837ca285f 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Helpers/ManagedIdentityTestUtil.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Helpers/ManagedIdentityTestUtil.cs @@ -32,7 +32,12 @@ public enum MsiAzureResource ServiceFabric } - public static void SetEnvironmentVariables(ManagedIdentitySource managedIdentitySource, string endpoint, string secret = "secret", string thumbprint = "thumbprint") + public static void SetEnvironmentVariables( + ManagedIdentitySource managedIdentitySource, + string endpoint, + string secret = "secret", + string thumbprint = "thumbprint", + string version = "version") { switch (managedIdentitySource) { @@ -58,6 +63,8 @@ public static void SetEnvironmentVariables(ManagedIdentitySource managedIdentity Environment.SetEnvironmentVariable("IDENTITY_ENDPOINT", endpoint); Environment.SetEnvironmentVariable("IDENTITY_HEADER", secret); Environment.SetEnvironmentVariable("IDENTITY_SERVER_THUMBPRINT", thumbprint); + Environment.SetEnvironmentVariable("APP_IDENTITY_ENDPOINT", endpoint); + Environment.SetEnvironmentVariable("IDENTITY_API_VERSION", version); break; case ManagedIdentitySource.MachineLearning: Environment.SetEnvironmentVariable("MSI_ENDPOINT", endpoint); diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs index 91e5c3d268..84cd0a1bd8 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs @@ -170,6 +170,15 @@ public static string GetMsiImdsErrorResponse() "\"correlation_id\":\"77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\",\"error_uri\":\"https://westus2.login.microsoft.com/error?code=500011\"}"; } + public static HttpResponseMessage CreateSuccessTokenResponseMessageForMits( + string accessToken = "some-access-token", + string expiresOn = "1744887386") + { + var stringContent = $"{{\"token_type\":\"Bearer\",\"access_token\":\"{accessToken}\",\"expires_on\":{expiresOn},\"resource\":\"api://AzureFMITokenExchange/.default\"}}"; + + return CreateSuccessResponseMessage(stringContent); + } + public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid) { return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}"); diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index bde5093fd3..fa0cbc2c3b 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -493,6 +493,43 @@ public static void AddManagedIdentityWSTrustMockHandler( }); } + public static void CreateFmiCredentialForMitsHandler( + this MockHttpManager httpManager, + string secret = "secret", + string version = "version", + string requestUri = "SomeUri", + string accessToken = "header.payload.signature", + bool expiredResponse = false + ) + { + string expiresOn; + DateTimeOffset dto = DateTimeOffset.UtcNow; + + if (expiredResponse) + { + long unixTimeSeconds = dto.ToUnixTimeSeconds() - 3600; + expiresOn = unixTimeSeconds.ToString(); + } + else + { + long unixTimeSeconds = dto.ToUnixTimeSeconds() + 3600; + expiresOn = unixTimeSeconds.ToString(); + } + + var handler = new MockHttpMessageHandler() + { + ExpectedUrl = requestUri, + ExpectedMethod = HttpMethod.Get, + ResponseMessage = MockHelpers.CreateSuccessTokenResponseMessageForMits(accessToken: accessToken, expiresOn: expiresOn), + ExpectedRequestHeaders = new Dictionary + { + { "Secret", secret }, + }, + }; + + httpManager.AddMockHandler(handler); + } + public static void AddRegionDiscoveryMockHandlerNotFound( this MockHttpManager httpManager) { diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs index 8dc79d1e99..301bf00cc0 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs @@ -91,6 +91,80 @@ public void ValidateServerCertificateCallback_ServerCertificateValidationCallbac } } + [TestMethod] + public void ValidateThatFmiEndpointIsUsed() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.ServiceFabric, "http://localhost:40342"); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithExperimentalFeatures() + .WithHttpManager(httpManager); + + var mi = miBuilder.BuildConcrete(); + + RequestContext requestContext = new RequestContext(mi.ServiceBundle, Guid.NewGuid(), null); + + ServiceFabricManagedIdentitySource sf = ServiceFabricManagedIdentitySource.Create(requestContext, true) as ServiceFabricManagedIdentitySource; + + Assert.IsInstanceOfType(sf, typeof(ServiceFabricManagedIdentitySource)); + Assert.AreEqual("http://localhost:40342/metadata/identity/oauth2/fmi/credential", sf.GetEndpointForTesting()); + } + } + + [TestMethod] + public async Task ValidateThatFmiCredentialCanBeAcquiredFromMits() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(managedIdentitySource: ManagedIdentitySource.ServiceFabric, + endpoint: "http://localhost:40343"); + + httpManager.CreateFmiCredentialForMitsHandler(requestUri: "http://localhost:40343/metadata/identity/oauth2/fmi/credential"); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithExperimentalFeatures() + .WithHttpManager(httpManager); + + var mi = miBuilder.BuildConcrete(); + + //Ensure token is acquired from MITS + var result = await mi.AcquireTokenForManagedIdentity("api://AzureFMITokenExchange/.default") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual("header.payload.signature", result.AccessToken); + + //Ensure token is acquired from cache + result = await mi.AcquireTokenForManagedIdentity("api://AzureFMITokenExchange/.default") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual("header.payload.signature", result.AccessToken); + } + } + + [TestMethod] + public async Task ValidateThatFmiCredentialIsExpiremental() + { + var miApp = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .Build(); + + var ex = await AssertException.TaskThrowsAsync( + () => miApp.AcquireTokenForManagedIdentity("api://AzureFMITokenExchange/.default") + .ExecuteAsync()).ConfigureAwait(false); + + Assert.IsNotNull(ex); + Assert.IsTrue(ex.Message.Contains("The API WithResource is marked as experimental")); + } + [TestMethod] public async Task SFThrowsWhenGetHttpClientWithValidationIsNotImplementedAsync() {