Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -78,46 +78,46 @@ public async Task<string> GetAzureRegionAsync(RequestContext requestContext)
requestContext.ApiEvent != null,
"Do not call GetAzureRegionAsync outside of a request. This can happen if you perform instance discovery outside a request, for example as part of validating input params.");

if (!IsAutoDiscoveryRequested(azureRegionConfig))
Comment thread
4gust marked this conversation as resolved.
{
// For a user-provided region (WithAzureRegion or the MSAL_FORCE_REGION env variable),
// validate the format before using it. An invalid region (e.g. one containing a host,
// path, or other special characters) must never be prefixed onto the trusted
// "{region}.login.microsoft.com" suffix, as that would redirect the request to a
// tampered host. Consistent with region handling elsewhere, an invalid value falls
// back to the global (non-regional) endpoint rather than failing the request.
if (!IsValidRegionName(azureRegionConfig))
{
logger.Error($"[Region discovery] User provided region '{azureRegionConfig}' is invalid. Falling back to the global endpoint. {DateTime.UtcNow}");
return null;
}
Comment thread
4gust marked this conversation as resolved.

logger.Info(() => $"[Region discovery] Returning user provided region: {azureRegionConfig}.");
requestContext.ApiEvent.RegionUsed = azureRegionConfig;
requestContext.ApiEvent.RegionOutcome = RegionOutcome.UserProvided;
return azureRegionConfig;
Comment thread
4gust marked this conversation as resolved.
}

IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory;
IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.RegionDiscovery);

// MSAL always performs region auto-discovery, even if the user configured an actual region
// in order to detect inconsistencies and report via telemetry
var discoveredRegion = await DiscoverAndCacheAsync(logger, requestContext.UserCancellationToken, retryPolicy).ConfigureAwait(false);

RecordTelemetry(requestContext.ApiEvent, azureRegionConfig, discoveredRegion);
RecordTelemetry(requestContext.ApiEvent, discoveredRegion);

if (IsAutoDiscoveryRequested(azureRegionConfig))
if (discoveredRegion.RegionSource != RegionAutodetectionSource.FailedAutoDiscovery)
{
if (discoveredRegion.RegionSource != RegionAutodetectionSource.FailedAutoDiscovery)
{
logger.Verbose(() => $"[Region discovery] Discovered Region {discoveredRegion.Region}");
requestContext.ApiEvent.RegionUsed = discoveredRegion.Region;
requestContext.ApiEvent.AutoDetectedRegion = discoveredRegion.Region;
return discoveredRegion.Region;
}
else
{
logger.Verbose(() => $"[Region discovery] {s_regionDiscoveryDetails}");
requestContext.ApiEvent.RegionDiscoveryFailureReason = s_regionDiscoveryDetails;
return null;
}
logger.Verbose(() => $"[Region discovery] Discovered Region {discoveredRegion.Region}");
requestContext.ApiEvent.RegionUsed = discoveredRegion.Region;
requestContext.ApiEvent.AutoDetectedRegion = discoveredRegion.Region;
return discoveredRegion.Region;
}

// For a user-provided region (WithAzureRegion or the MSAL_FORCE_REGION env variable),
// validate the format before using it. An invalid region (e.g. one containing a host,
// path, or other special characters) must never be prefixed onto the trusted
// "{region}.login.microsoft.com" suffix, as that would redirect the request to a
// tampered host. Consistent with region handling elsewhere, an invalid value falls
// back to the global (non-regional) endpoint rather than failing the request.
if (!IsValidRegionName(azureRegionConfig))
else
{
logger.Error($"[Region discovery] User provided region '{azureRegionConfig}' is invalid. Falling back to the global endpoint. {DateTime.UtcNow}");
logger.Verbose(() => $"[Region discovery] {s_regionDiscoveryDetails}");
requestContext.ApiEvent.RegionDiscoveryFailureReason = s_regionDiscoveryDetails;
return null;
}

logger.Info(() => $"[Region discovery] Returning user provided region: {azureRegionConfig}.");
return azureRegionConfig;
}

internal static void ResetStaticCacheForTest()
Expand All @@ -132,41 +132,19 @@ private static bool IsAutoDiscoveryRequested(string azureRegionConfig)
return string.Equals(azureRegionConfig, ConfidentialClientApplication.AttemptRegionDiscovery);
}

private static void RecordTelemetry(ApiEvent apiEvent, string azureRegionConfig, RegionInfo discoveredRegion)
private static void RecordTelemetry(ApiEvent apiEvent, RegionInfo discoveredRegion)
{
// already emitted telemetry for this request, don't emit again as it will overwrite with "from cache"
if (IsTelemetryRecorded(apiEvent))
{
return;
}

bool isAutoDiscoveryRequested = IsAutoDiscoveryRequested(azureRegionConfig);
apiEvent.RegionAutodetectionSource = discoveredRegion.RegionSource;

if (isAutoDiscoveryRequested)
{
apiEvent.RegionUsed = discoveredRegion.Region;
apiEvent.RegionOutcome = discoveredRegion.RegionSource == RegionAutodetectionSource.FailedAutoDiscovery ?
RegionOutcome.FallbackToGlobal :
RegionOutcome.AutodetectSuccess;
}
else
{
apiEvent.RegionUsed = azureRegionConfig;
apiEvent.RegionDiscoveryFailureReason = discoveredRegion.RegionDetails;

if (discoveredRegion.RegionSource == RegionAutodetectionSource.FailedAutoDiscovery)
{
apiEvent.RegionOutcome = RegionOutcome.UserProvidedAutodetectionFailed;
}

if (!string.IsNullOrEmpty(discoveredRegion.Region))
{
apiEvent.RegionOutcome = string.Equals(discoveredRegion.Region, azureRegionConfig, StringComparison.OrdinalIgnoreCase) ?
RegionOutcome.UserProvidedValid :
RegionOutcome.UserProvidedInvalid;
}
}
apiEvent.RegionUsed = discoveredRegion.Region;
apiEvent.RegionOutcome = discoveredRegion.RegionSource == RegionAutodetectionSource.FailedAutoDiscovery ?
RegionOutcome.FallbackToGlobal :
RegionOutcome.AutodetectSuccess;
}

private static bool IsTelemetryRecorded(ApiEvent apiEvent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,26 @@ namespace Microsoft.Identity.Client.Region
public enum RegionOutcome
{
/// <summary>
/// Indicates that the API .WithAzureRegion() was not used
/// Indicates that no region outcome was recorded.
/// </summary>
None = 0,

/// <summary>
/// Region provided by the user, matches auto detected region
/// Region provided by the user, matches auto detected region.
/// </summary>
[Obsolete("MSAL no longer performs auto-discovery for explicitly configured regions. Use UserProvided instead.", false)]
UserProvidedValid = 1,

/// <summary>
/// Region provided by the user, auto detection cannot be done
/// Region provided by the user, auto detection cannot be done.
/// </summary>
[Obsolete("MSAL no longer performs auto-discovery for explicitly configured regions. Use UserProvided instead.", false)]
UserProvidedAutodetectionFailed = 2,

/// <summary>
/// Region provided by the user, does not match auto detected region
/// Region provided by the user, does not match auto detected region.
/// </summary>
[Obsolete("MSAL no longer performs auto-discovery for explicitly configured regions. Use UserProvided instead.", false)]
UserProvidedInvalid = 3,

/// <summary>
Expand All @@ -38,6 +41,11 @@ public enum RegionOutcome
/// <summary>
/// Region autodetect requested but failed. Fallback to global
/// </summary>
FallbackToGlobal = 5
FallbackToGlobal = 5,

/// <summary>
/// Region provided by the user and used without auto-detection.
/// </summary>
UserProvided = 6
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Microsoft.Identity.Client.Region.RegionOutcome.UserProvided = 6 -> Microsoft.Identity.Client.Region.RegionOutcome
Original file line number Diff line number Diff line change
Expand Up @@ -197,18 +197,8 @@ public async Task FetchRegionFromLocalImdsThenGetMetadataFromCacheAsync()
}

[TestMethod]
[DataRow(HttpStatusCode.NotFound, 0, TestConstants.RegionAutoDetectNotFoundFailureMessage)] // No retries for 404 errors
[DataRow(HttpStatusCode.InternalServerError, TestRegionDiscoveryRetryPolicy.NumRetries, TestConstants.RegionAutoDetectInternalServerErrorFailureMessage)]
public async Task SuccessfulResponseFromUserProvidedRegionAsync(
HttpStatusCode statusCode,
int expectedRetries,
string expectedFailureMessage)
public async Task SuccessfulResponseFromUserProvidedRegionDoesNotCallImdsAsync()
{
for (int i = 0; i < (1 + expectedRetries); i++)
{
AddMockedResponse(MockHelpers.CreateNullMessage(statusCode));
}

_testRequestContext.ServiceBundle.Config.AzureRegion = TestConstants.Region;
RegionManager.ResetStaticCacheForTest();
IRegionDiscoveryProvider regionDiscoveryProvider = new RegionAndMtlsDiscoveryProvider(_httpManager);
Expand All @@ -219,48 +209,12 @@ public async Task SuccessfulResponseFromUserProvidedRegionAsync(
Assert.AreEqual($"centralus.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork);

Assert.AreEqual(TestConstants.Region, _testRequestContext.ApiEvent.RegionUsed);
Assert.AreEqual(RegionAutodetectionSource.FailedAutoDiscovery, _testRequestContext.ApiEvent.RegionAutodetectionSource);
Assert.AreEqual(RegionOutcome.UserProvidedAutodetectionFailed, _testRequestContext.ApiEvent.RegionOutcome);
Assert.Contains(expectedFailureMessage, _testRequestContext.ApiEvent.RegionDiscoveryFailureReason);

// Verify all mock responses were consumed
Assert.AreEqual(0, _httpManager.QueueSize);
}

[TestMethod]
public async Task ResponseFromUserProvidedRegionSameAsRegionDetectedAsync()
{
Environment.SetEnvironmentVariable(TestConstants.RegionName, TestConstants.Region);
_testRequestContext.ServiceBundle.Config.AzureRegion = TestConstants.Region;

// IRegionDiscoveryProvider regionDiscoveryProvider = new RegionDiscoveryProvider(_httpManager);
InstanceDiscoveryMetadataEntry regionalMetadata = await _regionDiscoveryProvider.GetMetadataAsync(new Uri("https://login.microsoftonline.com/common/"), _testRequestContext).ConfigureAwait(false);

Assert.IsNotNull(regionalMetadata);
Assert.AreEqual($"centralus.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork);
Assert.AreEqual(TestConstants.Region, _testRequestContext.ApiEvent.RegionUsed);
Assert.AreEqual(RegionAutodetectionSource.EnvVariable, _testRequestContext.ApiEvent.RegionAutodetectionSource);
Assert.AreEqual(RegionOutcome.UserProvidedValid, _testRequestContext.ApiEvent.RegionOutcome);
Assert.AreEqual(RegionAutodetectionSource.None, _testRequestContext.ApiEvent.RegionAutodetectionSource);
Assert.AreEqual(RegionOutcome.UserProvided, _testRequestContext.ApiEvent.RegionOutcome);
Assert.IsNull(_testRequestContext.ApiEvent.RegionDiscoveryFailureReason);
}

[TestMethod]
public async Task ResponseFromUserProvidedRegionDifferentFromRegionDetectedAsync()
{
Environment.SetEnvironmentVariable(TestConstants.RegionName, "detectedregion");
_testRequestContext.ServiceBundle.Config.AzureRegion = "userregion";

//IRegionDiscoveryProvider regionDiscoveryProvider = new RegionDiscoveryProvider(_httpManager, new NetworkCacheMetadataProvider());
InstanceDiscoveryMetadataEntry regionalMetadata = await _regionDiscoveryProvider.GetMetadataAsync(
new Uri("https://login.microsoftonline.com/common/"),
_testRequestContext).ConfigureAwait(false);

Assert.IsNotNull(regionalMetadata);
Assert.AreEqual($"userregion.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork);
Assert.AreEqual("userregion", _testRequestContext.ApiEvent.RegionUsed);
Assert.AreEqual(RegionAutodetectionSource.EnvVariable, _testRequestContext.ApiEvent.RegionAutodetectionSource);
Assert.AreEqual(RegionOutcome.UserProvidedInvalid, _testRequestContext.ApiEvent.RegionOutcome);
Assert.IsNull(_testRequestContext.ApiEvent.RegionDiscoveryFailureReason);
// Verify no IMDS request was made for the explicit region.
Assert.AreEqual(0, _httpManager.QueueSize);
}

[TestMethod]
Expand Down
Loading