Skip to content
Merged
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 @@ -60,7 +60,7 @@ private static bool TryValidateEnvVars(string msiEndpoint, string secret, ILogge
}
catch (FormatException ex)
{
throw new MsalClientException(MsalError.InvalidManagedIdentityEndpoint, string.Format(CultureInfo.InvariantCulture, MsiEndpointInvalidUriError, msiEndpoint), ex);
throw new MsalClientException(MsalError.InvalidManagedIdentityEndpoint, string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, msiEndpoint, "App service"), ex);
}

logger.Info($"[Managed Identity] Environment variables validation passed for app service managed identity. Endpoint uri: {endpointUri}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client.ManagedIdentity
{
/// <summary>
/// Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs
/// </summary>
internal class AzureArcManagedIdentitySource : ManagedIdentitySource
Comment thread
neha-bhargava marked this conversation as resolved.
{
private const string ArcApiVersion = "2019-11-01";

private readonly string _clientId;
private readonly string _resourceId;
private readonly Uri _endpoint;

public static ManagedIdentitySource TryCreate(RequestContext requestContext)
{
string identityEndpoint = EnvironmentVariables.IdentityEndpoint;
string imdsEndpoint = EnvironmentVariables.ImdsEndpoint;

// if BOTH the env vars IDENTITY_ENDPOINT and IMDS_ENDPOINT are set the MsiType is Azure Arc
if (string.IsNullOrEmpty(identityEndpoint) || string.IsNullOrEmpty(imdsEndpoint))
{
return null;
}

if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri))
{
throw new MsalClientException(MsalError.InvalidManagedIdentityEndpoint, string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, identityEndpoint, "Azure arc"));
}

return new AzureArcManagedIdentitySource(endpointUri, requestContext);
}

private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContext) : base(requestContext)
{
_endpoint = endpoint;
_clientId = requestContext.ServiceBundle.Config.ManagedIdentityUserAssignedClientId;
_resourceId = requestContext.ServiceBundle.Config.ManagedIdentityUserAssignedResourceId;

if (!string.IsNullOrEmpty(_clientId) || !string.IsNullOrEmpty(_resourceId))
{
throw new MsalClientException(MsalError.UserAssignedManagedIdentityNotSupported, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported);
Comment thread
neha-bhargava marked this conversation as resolved.
}
}

protected override ManagedIdentityRequest CreateRequest(string resource)
{
ManagedIdentityRequest request = new ManagedIdentityRequest(System.Net.Http.HttpMethod.Get, _endpoint);

request.Headers.Add("Metadata", "true");
request.QueryParameters["api-version"] = ArcApiVersion;
request.QueryParameters["resource"] = resource;

return request;
}

protected override async Task<ManagedIdentityResponse> HandleResponseAsync(
AppTokenProviderParameters parameters,
HttpResponse response,
CancellationToken cancellationToken)
{
_requestContext.Logger.Verbose($"[Managed Identity] Response received. Status code: {response.StatusCode}");

if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
Comment thread
neha-bhargava marked this conversation as resolved.
{
if (!response.HeadersAsDictionary.TryGetValue("WWW-Authenticate", out string challenge))
{
_requestContext.Logger.Error("[Managed Identity] WWW-Authenticate header is expected but not found.");
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, MsalErrorMessage.ManagedIdentityNoChallengeError);
}

var splitChallenge = challenge.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);

if (splitChallenge.Length != 2)
{
_requestContext.Logger.Error("[Managed Identity] The WWW-Authenticate header for Azure arc managed identity is not an expected format.");
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, MsalErrorMessage.ManagedIdentityInvalidChallange);
}

var authHeaderValue = "Basic " + File.ReadAllText(splitChallenge[1]);
Comment thread
neha-bhargava marked this conversation as resolved.

ManagedIdentityRequest request = CreateRequest(ScopeHelper.ScopesToResource(parameters.Scopes.ToArray()));
Comment thread
neha-bhargava marked this conversation as resolved.

_requestContext.Logger.Verbose("[Managed Identity] Adding authorization header to the request.");
request.Headers.Add("Authorization", authHeaderValue);

response = await _requestContext.ServiceBundle.HttpManager.SendGetAsync(request.ComputeUri(), request.Headers, _requestContext.Logger, cancellationToken: cancellationToken).ConfigureAwait(false);

return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}


return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ internal class EnvironmentVariables
public static string IdentityEndpoint => Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,10 @@ protected override ManagedIdentityRequest CreateRequest(string resource)
return request;
}

public override async Task<ManagedIdentityResponse> AuthenticateAsync(AppTokenProviderParameters parameters, CancellationToken cancellationToken)
{
try
{
return await base.AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
_requestContext.Logger.Error(TimeoutError);
throw;
}
}

protected override ManagedIdentityResponse HandleResponse(
protected override async Task<ManagedIdentityResponse> HandleResponseAsync(
AppTokenProviderParameters parameters,
HttpResponse response)
HttpResponse response,
CancellationToken cancellationToken)
{
// handle error status codes indicating managed identity is not available
var baseMessage = response.StatusCode switch
Expand All @@ -115,7 +103,7 @@ protected override ManagedIdentityResponse HandleResponse(
}

// Default behavior to handle successful scenario and general errors.
return base.HandleResponse(parameters, response);
return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}

internal static string CreateRequestFailedMessage(HttpResponse response, string message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal async Task<AppTokenProviderResult> AppTokenProviderImplAsync(AppTokenPr
private static ManagedIdentitySource SelectManagedIdentitySource(RequestContext requestContext)
{
return AppServiceManagedIdentitySource.TryCreate(requestContext) ??
AzureArcManagedIdentitySource.TryCreate(requestContext) ??
new ImdsManagedIdentitySource(requestContext);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(AppTokenPro
await _requestContext.ServiceBundle.HttpManager.SendGetForceResponseAsync(request.ComputeUri(), request.Headers, _requestContext.Logger, cancellationToken: cancellationToken).ConfigureAwait(false) :
await _requestContext.ServiceBundle.HttpManager.SendPostForceResponseAsync(request.ComputeUri(), request.Headers, request.BodyParameters, _requestContext.Logger, cancellationToken: cancellationToken).ConfigureAwait(false);

return HandleResponse(parameters, response);
return await HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}
catch(TaskCanceledException)
{
Expand All @@ -57,9 +57,10 @@ await _requestContext.ServiceBundle.HttpManager.SendGetForceResponseAsync(reques
}
}

protected virtual ManagedIdentityResponse HandleResponse(
protected virtual async Task<ManagedIdentityResponse> HandleResponseAsync(
AppTokenProviderParameters parameters,
HttpResponse response)
HttpResponse response,
CancellationToken cancellationToken)
{
string message;
Exception exception = null;
Expand All @@ -79,7 +80,7 @@ protected virtual ManagedIdentityResponse HandleResponse(
{
_requestContext.Logger.Error($"[Managed Identity] Exception: {e.Message} Http status code: {response?.StatusCode}");
exception = e;
message = MsalErrorMessage.UnexpectedResponse;
message = MsalErrorMessage.ManagedIdentityUnexpectedResponse;
}

throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, message, exception);
Expand All @@ -94,7 +95,7 @@ protected ManagedIdentityResponse GetSuccessfulResponse(HttpResponse response)
if (managedIdentityResponse == null || managedIdentityResponse.AccessToken.IsNullOrEmpty() || managedIdentityResponse.ExpiresOn.IsNullOrEmpty())
{
_requestContext.Logger.Error("[Managed Identity] Response is either null or insufficient for authentication.");
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, MsalErrorMessage.AuthenticationResponseInvalidFormatError);
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, MsalErrorMessage.ManagedIdentityInvalidResponse);
}

return managedIdentityResponse;
Expand All @@ -106,7 +107,7 @@ internal string GetMessageFromErrorResponse(HttpResponse response)

if (managedIdentityErrorResponse == null)
{
return "[Managed Identity] Authentication unavailable. No response received from the managed identity endpoint.";
return MsalErrorMessage.ManagedIdentityNoResponseReceived;
}

if (!string.IsNullOrEmpty(managedIdentityErrorResponse.Message))
Expand Down
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 @@ -1125,5 +1125,11 @@ public static class MsalError
/// Exactly one scope is expected.
/// </summary>
public const string ExactlyOneScopeExpected = "exactly_one_scope_expected";

/// <summary>
/// User assigned managed identity is not supported for this source.
/// </summary>
public const string UserAssignedManagedIdentityNotSupported = "user_assigned_managed_identity_not_supported";

}
}
13 changes: 10 additions & 3 deletions src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,15 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
invalidValueName);
}

public const string AuthenticationResponseInvalidFormatError = "[Managed Identity] Invalid response, the authentication response received did not contain the expected fields.";
public const string UnexpectedResponse = "[Managed Identity] Response was not in the expected format. See the inner exception for details.";
public const string ExactlyOneScopeExpected = "[Managed Identity] To acquire token for managed identity, exactly one scope should be passed.";
public const string ManagedIdentityNoResponseReceived = "[Managed Identity] Authentication unavailable. No response received from the managed identity endpoint.";
public const string ManagedIdentityInvalidResponse = "[Managed Identity] Invalid response, the authentication response received did not contain the expected fields.";
public const string ManagedIdentityUnexpectedResponse = "[Managed Identity] Unexpected exception occurred when parsing the response. See the inner exception for details.";
public const string ManagedIdentityExactlyOneScopeExpected = "[Managed Identity] To acquire token for managed identity, exactly one scope must be passed.";

public const string ManagedIdentityEndpointInvalidUriError = "[Managed Identity] The environment variable IDENTITY_ENDPOINT contains an invalid Uri {0} 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 ManagedIdentityInvalidChallange = "[Managed Identity] The WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint did not match the expected format.";
public const string ManagedIdentityUserAssignedNotSupported = "[Managed Identity] User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate with the system assigned identity omit the client id to .WithManagedIentity().";

}
}
4 changes: 2 additions & 2 deletions src/client/Microsoft.Identity.Client/Utils/ScopeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ public static string ScopesToResource(string[] scopes)
{
if (scopes == null)
{
throw new MsalClientException(MsalError.ExactlyOneScopeExpected, MsalErrorMessage.ExactlyOneScopeExpected);
throw new MsalClientException(MsalError.ExactlyOneScopeExpected, MsalErrorMessage.ManagedIdentityExactlyOneScopeExpected);
}

if (scopes.Length != 1)
{
throw new MsalClientException(MsalError.ExactlyOneScopeExpected, MsalErrorMessage.ExactlyOneScopeExpected);
throw new MsalClientException(MsalError.ExactlyOneScopeExpected, MsalErrorMessage.ManagedIdentityExactlyOneScopeExpected);
}

if (!scopes[0].EndsWith(DefaultSuffix, StringComparison.Ordinal))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,16 @@ public static void AddManagedIdentityMockHandler(

IDictionary<string, string> expectedRequestHeaders = new Dictionary<string, string>();

if (managedIdentitySourceType != ManagedIdentitySourceType.IMDS)
if (managedIdentitySourceType == ManagedIdentitySourceType.AppService)
{
expectedRequestHeaders.Add("X-IDENTITY-HEADER", "secret");
}


if (managedIdentitySourceType == ManagedIdentitySourceType.IMDS || managedIdentitySourceType == ManagedIdentitySourceType.AzureArc)
{
expectedRequestHeaders.Add("Metadata", "true");
}

if (userAssignedIdentityId == UserAssignedIdentityId.ClientId)
{
expectedQueryParams.Add("client_id", userAssignedClientIdOrResourceId);
Expand All @@ -378,6 +383,26 @@ public static void AddManagedIdentityMockHandler(
});
}

public static void AddManagedIdentityWSTrustMockHandler(
this MockHttpManager httpManager,
string expectedUrl,
string filePath = null)
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
if (filePath != null)
{
responseMessage.Headers.Add("WWW-Authenticate", $"Basic realm={filePath}");
}

httpManager.AddMockHandler(
new MockHttpMessageHandler
{
ExpectedMethod = HttpMethod.Get,
ExpectedUrl = expectedUrl,
ResponseMessage = responseMessage
});
}

public static void AddRegionDiscoveryMockHandlerNotFound(
this MockHttpManager httpManager)
{
Expand Down Expand Up @@ -421,6 +446,7 @@ public enum UserAssignedIdentityId
public enum ManagedIdentitySourceType
{
IMDS,
AppService
AppService,
AzureArc
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ await cca.AcquireTokenForClient(new string[] { "https://management.azure.com" })

Assert.IsNotNull(ex);
Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode);
Assert.AreEqual(MsalErrorMessage.AuthenticationResponseInvalidFormatError, ex.Message);
Assert.AreEqual(MsalErrorMessage.ManagedIdentityInvalidResponse, ex.Message);
}

}
Expand Down
Loading