Skip to content

Commit

Permalink
Refactor Exception Handling: Transition from ManagedIdentityException…
Browse files Browse the repository at this point in the history
… to MsalServiceException (#4476)

* initial commit

* pr comments

* update per Bogdans comment

* pr comments

* Apply suggestions from code review

Co-authored-by: Peter <[email protected]>

* readonly

* address comments

* message

* message

* Update tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs

Co-authored-by: Peter <[email protected]>

* test update

* no rethrow

---------

Co-authored-by: Gladwin Johnson <[email protected]>
Co-authored-by: Peter <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2024
1 parent 1ce5ac0 commit 9e18005
Show file tree
Hide file tree
Showing 18 changed files with 316 additions and 126 deletions.
48 changes: 34 additions & 14 deletions src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,45 @@ namespace Microsoft.Identity.Client.AppConfig
/// </summary>
public class ManagedIdentityId
{
/// <summary>
/// Gets the identifier for a user-assigned managed identity.
/// </summary>
/// <remarks>
/// This property holds the unique identifier of the user-assigned managed identity.
/// It can be a client ID, a resource ID, or an object ID, depending on how the managed identity is configured.
/// </remarks>
/// <value>
/// The identifier string of the user-assigned managed identity.
/// </value>
internal string UserAssignedId { get; private set; }
internal ManagedIdentityIdType IdType { get; private set; }
internal readonly bool _isUserAssigned;

/// <summary>
/// Gets the type of identifier used for the managed identity.
/// </summary>
/// <remarks>
/// This property indicates the type of the managed identity identifier,
/// which can be either a client ID, a resource ID, or an object ID.
/// </remarks>
/// <value>
/// The enumeration value representing the managed identity identifier type.
/// </value>
internal ManagedIdentityIdType IdType { get; }

/// <summary>
/// Gets a value indicating whether the managed identity is user-assigned.
/// </summary>
/// <remarks>
/// This property is true if the managed identity is user-assigned, and false if it is system-assigned.
/// </remarks>
/// <value>
/// True if the managed identity is user-assigned; otherwise, false.
/// </value>
internal bool IsUserAssigned { get; }

private ManagedIdentityId(ManagedIdentityIdType idType)
{
IdType = idType;

switch (idType)
{
case ManagedIdentityIdType.SystemAssigned:
_isUserAssigned = false;
break;
case ManagedIdentityIdType.ClientId:
case ManagedIdentityIdType.ResourceId:
case ManagedIdentityIdType.ObjectId:
_isUserAssigned = true;
break;
}
IsUserAssigned = idType != ManagedIdentityIdType.SystemAssigned;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Core;
using System.Net;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using System.Net.Sockets;
using System.Collections.Generic;

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/ManagedIdentitySource.cs
/// </summary>
internal abstract class AbstractManagedIdentity

{
protected readonly RequestContext _requestContext;
internal const string TimeoutError = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";
Expand Down Expand Up @@ -67,13 +64,9 @@ await _requestContext.ServiceBundle.HttpManager

return await HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
throw new MsalManagedIdentityException(MsalError.ManagedIdentityUnreachableNetwork, ex.Message, ex.InnerException, _sourceType);
}
catch (TaskCanceledException)
catch (Exception ex)
{
_requestContext.Logger.Error(TimeoutError);
HandleException(ex);
throw;
}
}
Expand All @@ -83,28 +76,24 @@ protected virtual Task<ManagedIdentityResponse> HandleResponseAsync(
HttpResponse response,
CancellationToken cancellationToken)
{
string message;
Exception exception = null;

try
{
if (response.StatusCode == HttpStatusCode.OK)
{
_requestContext.Logger.Info("[Managed Identity] Successful response received.");
return Task.FromResult(GetSuccessfulResponse(response));
}

message = GetMessageFromErrorResponse(response);
_requestContext.Logger.Error($"[Managed Identity] request failed, HttpStatusCode: {response.StatusCode} Error message: {message}");
}
catch (Exception e) when (e is not MsalManagedIdentityException)
if (response.StatusCode == HttpStatusCode.OK)
{
_requestContext.Logger.Error($"[Managed Identity] Exception: {e.Message} Http status code: {response?.StatusCode}");
exception = e;
message = MsalErrorMessage.ManagedIdentityUnexpectedResponse;
_requestContext.Logger.Info("[Managed Identity] Successful response received.");
return Task.FromResult(GetSuccessfulResponse(response));
}

throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed, message, exception, _sourceType, (int)response.StatusCode);
string message = GetMessageFromErrorResponse(response);

_requestContext.Logger.Error($"[Managed Identity] request failed, HttpStatusCode: {response.StatusCode} Error message: {message}");

MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
message,
null,
_sourceType,
(int)response.StatusCode);

throw exception;
}

protected abstract ManagedIdentityRequest CreateRequest(string resource);
Expand All @@ -116,18 +105,23 @@ 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 MsalManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityInvalidResponse,
_sourceType);

var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityInvalidResponse,
null,
_sourceType,
null);

throw exception;
}

return managedIdentityResponse;
}

internal string GetMessageFromErrorResponse(HttpResponse response)
{
var managedIdentityErrorResponse = JsonHelper.TryToDeserializeFromJson<ManagedIdentityErrorResponse>(response?.Body);
ManagedIdentityErrorResponse managedIdentityErrorResponse = JsonHelper.TryToDeserializeFromJson<ManagedIdentityErrorResponse>(response?.Body);

if (managedIdentityErrorResponse == null)
{
Expand All @@ -141,5 +135,47 @@ internal string GetMessageFromErrorResponse(HttpResponse response)

return $"[Managed Identity] Error Code: {managedIdentityErrorResponse.Error} Error Message: {managedIdentityErrorResponse.ErrorDescription}";
}

private void HandleException(Exception ex,
ManagedIdentitySource managedIdentitySource = ManagedIdentitySource.None,
string additionalInfo = null)
{
ManagedIdentitySource source = managedIdentitySource != ManagedIdentitySource.None ? managedIdentitySource : _sourceType;

if (ex is HttpRequestException httpRequestException)
{
CreateAndThrowException(MsalError.ManagedIdentityUnreachableNetwork, httpRequestException.Message, httpRequestException, source);
}
else if (ex is TaskCanceledException)
{
_requestContext.Logger.Error(TimeoutError);
}
else if (ex is FormatException formatException)
{
string errorMessage = additionalInfo ?? formatException.Message;
_requestContext.Logger.Error($"[Managed Identity] Format Exception: {errorMessage}");
CreateAndThrowException(MsalError.InvalidManagedIdentityEndpoint, errorMessage, formatException, source);
}
else if (ex is not MsalServiceException or TaskCanceledException)
{
_requestContext.Logger.Error($"[Managed Identity] Exception: {ex.Message}");
CreateAndThrowException(MsalError.ManagedIdentityRequestFailed, ex.Message, ex, source);
}
}

private void CreateAndThrowException(string errorCode,
string errorMessage,
Exception innerException,
ManagedIdentitySource source)
{
MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
errorCode,
errorMessage,
innerException,
source,
null);

throw exception;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,20 @@ private static bool TryValidateEnvVars(string msiEndpoint, string secret, ILogge
}
catch (FormatException ex)
{
throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format(
CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", msiEndpoint, "App Service"),
ex, ManagedIdentitySource.AppService);
string errorMessage = string.Format(
CultureInfo.InvariantCulture,
MsalErrorMessage.ManagedIdentityEndpointInvalidUriError,
"IDENTITY_ENDPOINT", msiEndpoint, "App Service");

// Use the factory to create and throw the exception
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
ex,
ManagedIdentitySource.AppService,
null); // statusCode is null in this case

throw exception;
}

logger.Info($"[Managed Identity] Environment variables validation passed for app service managed identity. Endpoint URI: {endpointUri}. Creating App Service managed identity.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,20 @@ public static AbstractManagedIdentity TryCreate(RequestContext requestContext)

if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri))
{
throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format(
CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", identityEndpoint, AzureArc),
ManagedIdentitySource.AzureArc);
string errorMessage = string.Format(
CultureInfo.InvariantCulture,
MsalErrorMessage.ManagedIdentityEndpointInvalidUriError,
"IDENTITY_ENDPOINT", identityEndpoint, AzureArc);

// Use the factory to create and throw the exception
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
null,
ManagedIdentitySource.AzureArc,
null);

throw exception;
}

requestContext.Logger.Verbose(()=>"[Managed Identity] Creating Azure Arc managed identity. Endpoint URI: " + endpointUri);
Expand All @@ -54,11 +65,18 @@ private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContex
{
_endpoint = endpoint;

if (requestContext.ServiceBundle.Config.ManagedIdentityId._isUserAssigned)
if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned)
{
throw new MsalManagedIdentityException(MsalError.UserAssignedManagedIdentityNotSupported,
string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc),
ManagedIdentitySource.AzureArc);
string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc);

var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.UserAssignedManagedIdentityNotSupported,
errorMessage,
null,
ManagedIdentitySource.AzureArc,
null);

throw exception;
}
}

Expand All @@ -85,19 +103,31 @@ protected override async Task<ManagedIdentityResponse> HandleResponseAsync(
if (!response.HeadersAsDictionary.TryGetValue("WWW-Authenticate", out string challenge))
{
_requestContext.Logger.Error("[Managed Identity] WWW-Authenticate header is expected but not found.");
throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityNoChallengeError,
ManagedIdentitySource.AzureArc);

var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityNoChallengeError,
null,
ManagedIdentitySource.AzureArc,
null);

throw exception;
}

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 MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityInvalidChallenge,
ManagedIdentitySource.AzureArc);

var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityInvalidChallenge,
null,
ManagedIdentitySource.AzureArc,
null);

throw exception;
}

var authHeaderValue = "Basic " + File.ReadAllText(splitChallenge[1]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,21 @@ public static AbstractManagedIdentity TryCreate(RequestContext requestContext)
catch (FormatException ex)
{
requestContext.Logger.Error("[Managed Identity] Invalid endpoint found for the environment variable MSI_ENDPOINT: " + msiEndpoint);
throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format(
CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "MSI_ENDPOINT", msiEndpoint, CloudShell),
ex, ManagedIdentitySource.CloudShell);

string errorMessage = string.Format(
CultureInfo.InvariantCulture,
MsalErrorMessage.ManagedIdentityEndpointInvalidUriError,
"MSI_ENDPOINT", msiEndpoint, CloudShell);

// Use the factory to create and throw the exception
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
ex,
ManagedIdentitySource.CloudShell,
null);

throw exception;
}

requestContext.Logger.Verbose(()=>"[Managed Identity] Creating cloud shell managed identity. Endpoint URI: " + msiEndpoint);
Expand All @@ -50,11 +62,21 @@ private CloudShellManagedIdentitySource(Uri endpoint, RequestContext requestCont
{
_endpoint = endpoint;

if (requestContext.ServiceBundle.Config.ManagedIdentityId._isUserAssigned)
if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned)
{
throw new MsalManagedIdentityException(MsalError.UserAssignedManagedIdentityNotSupported,
string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, CloudShell),
ManagedIdentitySource.CloudShell);
string errorMessage = string.Format(
CultureInfo.InvariantCulture,
MsalErrorMessage.ManagedIdentityUserAssignedNotSupported,
CloudShell);

var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.UserAssignedManagedIdentityNotSupported,
errorMessage,
null,
ManagedIdentitySource.CloudShell,
null);

throw exception;
}
}

Expand Down
Loading

0 comments on commit 9e18005

Please sign in to comment.