Skip to content

Adding FMI source to MI app #5299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an extra param here? Can't ManagedIdentity classes deal with this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can do that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it is better to keep this here because there the check is needed for the experimental features to be enabled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with dropping the experimental feature flag if it leads to better design.

ValidateUseOfExperimentalFeature();
}

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private async Task<AuthenticationResult> SendTokenRequestForManagedIdentityAsync
await ResolveAuthorityAsync().ConfigureAwait(false);

ManagedIdentityClient managedIdentityClient =
new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext);
new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext, _managedIdentityParameters);

ManagedIdentityResponse managedIdentityResponse =
await managedIdentityClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -35,11 +35,16 @@ internal Task<ManagedIdentityResponse> 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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpClient> _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<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> GetValidationCallback()
Expand All @@ -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)
{
Expand All @@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt there be a default case? We could log the unexpected, that could give us additional information when troubleshooting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically yes, but this ID is a required parameter for the managed identity application. So it will always be set to something unless there is a catastrophic failure.

Copy link
Member

@bgavrilMS bgavrilMS Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still a good practice to throw an exception @trwalke on default. It will protect against a case where a new enum parameter is added.

{
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();
}
}
}
1 change: 1 addition & 0 deletions src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "\"}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
{
{ "Secret", secret },
},
};

httpManager.AddMockHandler(handler);
}

public static void AddRegionDiscoveryMockHandlerNotFound(
this MockHttpManager httpManager)
{
Expand Down
Loading