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..d21a8f97da 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,11 @@ 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) { return GetManagedIdentitySource(requestContext.Logger) switch { - ManagedIdentitySource.ServiceFabric => ServiceFabricManagedIdentitySource.Create(requestContext), + ManagedIdentitySource.ServiceFabric => ServiceFabricManagedIdentitySource.Create(requestContext, acquireTokenForManagedIdentityParameters.Resource), 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..b2c971dbb1 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs @@ -16,34 +16,86 @@ 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, string resource) { + Uri endpointUri; string identityEndpoint = EnvironmentVariables.IdentityEndpoint; + bool isFmiCredentialRequest = false; - requestContext.Logger.Info(() => "[Managed Identity] Service fabric managed identity is available."); + if(resource is not null && resource.Equals("api://AzureFMITokenExchange/.default", StringComparison.OrdinalIgnoreCase)) + { + isFmiCredentialRequest = true; + requestContext.Logger.Info(() => "[Managed Identity] Request is for FMI, using federated managed identity."); + } - if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri)) + if (isFmiCredentialRequest) { - 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; + 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 + { + 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 {(isFmiCredentialRequest ? "federated" : "")} managed identity. Endpoint URI: {identityEndpoint}"); - requestContext.Logger.Verbose(() => "[Managed Identity] Creating Service Fabric managed identity. Endpoint URI: " + identityEndpoint); - - return new ServiceFabricManagedIdentitySource(requestContext, endpointUri, EnvironmentVariables.IdentityHeader); + 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 +114,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 +130,47 @@ 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) + { + _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 { - 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; + 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; + default: + throw new MsalServiceException( + MsalError.InvalidManagedIdentityIdType, + MsalErrorMessage.ManagedIdentityInvalidIdTypeError); + } } return request; } + + internal string GetEndpointForTesting() + { + return _endpoint.ToString(); + } } } diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index e712c02c77..4c53d4d2ba 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1188,5 +1188,11 @@ public static class MsalError /// - If token hashing is required, allow the cached token to be used instead of forcing a refresh. /// public const string ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible"; + + /// + /// What happened? The specified managed identity IdType is invalid + /// Mitigation: Ensure a valid IdType is specified for the managed identity. + /// + public const string InvalidManagedIdentityIdType = "invalid_managed_identity_id_type"; } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index b1895118aa..a5278f4808 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."; @@ -438,5 +439,6 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string MtlsNonTenantedAuthorityNotAllowedMessage = "mTLS authentication requires a tenanted authority. Using 'common', 'organizations', or similar non-tenanted authorities is not allowed. Please provide an authority with a specific tenant ID (e.g., 'https://login.microsoftonline.com/{tenantId}'). See https://aka.ms/msal-net-pop for details."; public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details."; public const string ForceRefreshAndTokenHasNotCompatible = "Cannot specify ForceRefresh and AccessTokenSha256ToRefresh in the same request."; + public const string ManagedIdentityInvalidIdTypeError = "[Managed Identity] The Managed Identity Id type is invalid. It must be either 'objectId' 'resourceId' or 'clientId'."; } } 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 8b13789179..bfda2c78d8 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 @@ - +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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 8b13789179..bfda2c78d8 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 @@ - +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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..bfda2c78d8 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 @@ +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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..bfda2c78d8 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 @@ +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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 8b13789179..bfda2c78d8 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 @@ - +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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..bfda2c78d8 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 @@ +const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string 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/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index d8d385e848..277feafbdb 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -1398,7 +1398,7 @@ private AbstractManagedIdentity CreateManagedIdentitySource(ManagedIdentitySourc switch (sourceType) { case ManagedIdentitySource.ServiceFabric: - managedIdentity = ServiceFabricManagedIdentitySource.Create(requestContext); + managedIdentity = ServiceFabricManagedIdentitySource.Create(requestContext, ""); break; case ManagedIdentitySource.AppService: managedIdentity = AppServiceManagedIdentitySource.Create(requestContext); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs index 8dc79d1e99..18ed394813 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs @@ -83,7 +83,7 @@ public void ValidateServerCertificateCallback_ServerCertificateValidationCallbac RequestContext requestContext = new RequestContext(mi.ServiceBundle, Guid.NewGuid(), null); - var sf = ServiceFabricManagedIdentitySource.Create(requestContext); + var sf = ServiceFabricManagedIdentitySource.Create(requestContext, ""); Assert.IsInstanceOfType(sf, typeof(ServiceFabricManagedIdentitySource)); var callback = sf.GetValidationCallback(); @@ -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, "api://AzureFMITokenExchange/.default") 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() {