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 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,11 @@ 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)
{
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

I recommend you park for now this or sync with @Robbie-Microsoft who's refactoring this completely.

Copy link
Member Author

Choose a reason for hiding this comment

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

ack

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, 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);
Copy link
Member

Choose a reason for hiding this comment

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

YOu can have 1 request for FMI credential and 1 request for normal token. So this won't work.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what you mean by this. The client will only do one or the other. The FMI version of this will only be used by MISE for the credential.

}

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 +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)
{
Expand All @@ -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();
}
}
}
6 changes: 6 additions & 0 deletions src/client/Microsoft.Identity.Client/MsalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public const string ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible";

/// <summary>
/// <para>What happened?</para> The specified managed identity IdType is invalid
/// <para>Mitigation:</para> Ensure a valid IdType is specified for the managed identity.
/// </summary>
public const string InvalidManagedIdentityIdType = "invalid_managed_identity_id_type";
}
}
2 changes: 2 additions & 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 All @@ -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'.";
}
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@

const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
Original file line number Diff line number Diff line change
@@ -1 +1 @@

const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
Original file line number Diff line number Diff line change
@@ -1 +1 @@

const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string
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
Loading