diff --git a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs index 7f32a2fb85..97dc7fcceb 100644 --- a/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs +++ b/src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs @@ -98,6 +98,14 @@ internal static class AppContextSwitches private static bool? _useCapitalizedXMLTypeAttr; internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr); + /// + /// When enabled, telemetry will use the full metadata address instead of just the domain name for IdentityModelConfiguration metrics. + /// By default (when disabled), only the domain name is used for successful operations to reduce OpenTelemetry cardinality. + /// + internal const string UseFullMetadataAddressForTelemetrySwitch = "Switch.Microsoft.IdentityModel.UseFullMetadataAddressForTelemetry"; + private static bool? _useFullMetadataAddressForTelemetry; + internal static bool UseFullMetadataAddressForTelemetry => _useFullMetadataAddressForTelemetry ??= (AppContext.TryGetSwitch(UseFullMetadataAddressForTelemetrySwitch, out bool useFullMetadataAddressForTelemetry) && useFullMetadataAddressForTelemetry); + /// /// Used for testing to reset all switches to its default value. /// @@ -123,6 +131,9 @@ internal static void ResetAllSwitches() _useCapitalizedXMLTypeAttr = null; AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false); + + _useFullMetadataAddressForTelemetry = null; + AppContext.SetSwitch(UseFullMetadataAddressForTelemetrySwitch, false); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs index 2a56774454..73ad2100a1 100644 --- a/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs +++ b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs @@ -22,12 +22,33 @@ internal class TelemetryClient : ITelemetryClient AppContextSwitches.UpdateConfigAsBlocking.ToString() ); + /// + /// Extracts the domain name from a metadata address for telemetry purposes. + /// Returns the full address if domain extraction fails or if the UseFullMetadataAddressForTelemetry switch is enabled. + /// In error cases, always returns the full address for better debugging. + /// + /// The full metadata address + /// True if this is a successful operation, false for error cases + /// Domain name for success cases (when switch is disabled), full address otherwise + internal static string GetMetadataAddressForTelemetry(string metadataAddress, bool isSuccessCase = true) + { + // Always use full address for error cases or when the switch is enabled + if (!isSuccessCase || AppContextSwitches.UseFullMetadataAddressForTelemetry || string.IsNullOrEmpty(metadataAddress)) + return metadataAddress; + + + if (Uri.TryCreate(metadataAddress, UriKind.Absolute, out Uri result)) + return result.Host; + + return metadataAddress; + } + public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, string configurationSource) { var tagList = new TagList() { { TelemetryConstants.IdentityModelVersionTag, ClientVer }, - { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: true) }, { TelemetryConstants.OperationStatusTag, operationStatus }, { TelemetryConstants.ConfigurationSourceTag, configurationSource }, _blockingTagValue @@ -41,7 +62,7 @@ public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, var tagList = new TagList() { { TelemetryConstants.IdentityModelVersionTag, ClientVer }, - { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) }, { TelemetryConstants.OperationStatusTag, operationStatus }, { TelemetryConstants.ConfigurationSourceTag, configurationSource }, { TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() }, @@ -56,7 +77,7 @@ public void LogConfigurationRetrievalDuration(string metadataAddress, string con var tagList = new TagList() { { TelemetryConstants.IdentityModelVersionTag, ClientVer }, - { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: true) }, { TelemetryConstants.ConfigurationSourceTag, configurationSource }, }; @@ -69,7 +90,7 @@ public void LogConfigurationRetrievalDuration(string metadataAddress, string con var tagList = new TagList() { { TelemetryConstants.IdentityModelVersionTag, ClientVer }, - { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) }, { TelemetryConstants.ConfigurationSourceTag, configurationSource }, { TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() }, _blockingTagValue @@ -87,7 +108,7 @@ public void LogBackgroundConfigurationRefreshFailure( var tagList = new TagList() { { TelemetryConstants.IdentityModelVersionTag, ClientVer }, - { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) }, { TelemetryConstants.ConfigurationSourceTag, configurationSource }, { TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() }, _blockingTagValue diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/ClaimsIdentityFactoryTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/ClaimsIdentityFactoryTests.cs index f1878a638b..1a1cf0da39 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/ClaimsIdentityFactoryTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/ClaimsIdentityFactoryTests.cs @@ -9,14 +9,10 @@ namespace Microsoft.IdentityModel.Tokens.Tests { - [Collection(nameof(ClaimsIdentityFactoryTests))] + [ResetAppContextSwitches] + [Collection("AppContextSwitches")] public class ClaimsIdentityFactoryTests { - public ClaimsIdentityFactoryTests() - { - AppContextSwitches.ResetAllSwitches(); - } - [Theory] [InlineData(true)] [InlineData(false)] @@ -47,7 +43,7 @@ public void Create_FromTokenValidationParameters_ReturnsCorrectClaimsIdentity(bo Assert.Equal(jsonWebToken, ((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken); } - AppContextSwitches.ResetAllSwitches(); + AppContext.SetSwitch(AppContextSwitches.UseClaimsIdentityTypeSwitch, false); } [Theory] diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/TelemetryClientDomainExtractionTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/TelemetryClientDomainExtractionTests.cs new file mode 100644 index 0000000000..4cba8625b5 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/TelemetryClientDomainExtractionTests.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.Telemetry.Tests; + +[ResetAppContextSwitches] +[Collection("AppContextSwitches")] +public class TelemetryClientDomainExtractionTests +{ + [Theory] + [InlineData("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "login.microsoftonline.com")] + [InlineData("https://www.login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "www.login.microsoftonline.com")] + [InlineData("https://accounts.google.com/.well-known/openid-configuration", "accounts.google.com")] + [InlineData("https://login.windows.net/common/.well-known/openid-configuration", "login.windows.net")] + [InlineData("http://localhost:8080/.well-known/openid-configuration", "localhost")] + [InlineData("https://example.com/path/to/config", "example.com")] + [InlineData("https://subdomain.example.org/config.json", "subdomain.example.org")] + public void GetMetadataAddressForTelemetry_SuccessCase_ReturnsExpectedDomain(string fullAddress, string expectedDomain) + { + // Act + var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, true); + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Fact] + public void DebugAppContextSwitch() + { + // Check if the AppContext switch is causing issues + var switchValue = AppContextSwitches.UseFullMetadataAddressForTelemetry; + + var testUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + var result = TelemetryClient.GetMetadataAddressForTelemetry(testUrl); + + // The switch should be false by default, so we should get the host name + Assert.False(switchValue, $"Switch value: {switchValue}"); + Assert.Equal("login.microsoftonline.com", result); + } + + [Theory] + [InlineData("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration")] + [InlineData("https://accounts.google.com/.well-known/openid-configuration")] + [InlineData("https://login.windows.net/common/.well-known/openid-configuration")] + public void GetMetadataAddressForTelemetry_ErrorCase_ReturnsFullAddress(string fullAddress) + { + // Act + var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, false); + + // Assert + Assert.Equal(fullAddress, result); + } + + [Theory] + [InlineData("invalid-url")] + [InlineData("")] + [InlineData(null)] + public void GetMetadataAddressForTelemetry_InvalidUrl_ReturnsOriginalString(string invalidUrl) + { + // Act + var result = TelemetryClient.GetMetadataAddressForTelemetry(invalidUrl, true); + + // Assert + Assert.Equal(invalidUrl, result); + } + + [Fact] + public void GetMetadataAddressForTelemetry_WithAppContextSwitch_ReturnsFullAddress() + { + // Arrange + var fullAddress = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + + try + { + // Enable the switch to use full metadata address + AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, true); + + // Act + var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, true); + + // Assert + Assert.Equal(fullAddress, result); + } + finally + { + // Cleanup is handled by ResetAppContextSwitches attribute + AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, false); + } + } + + [Fact] + public void TelemetryClient_Methods_DoNotThrow() + { + // This test ensures that the modified telemetry methods don't break existing functionality + // We cannot easily verify the exact values sent to the telemetry system without complex setup, + // but we can ensure the methods don't throw exceptions when called with valid parameters. + + // Arrange + var telemetryClient = new TelemetryClient(); + var testAddress = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + var testDuration = TimeSpan.FromMilliseconds(100); + var testException = new InvalidOperationException("Test exception"); + + // Act & Assert - these should not throw + var ex1 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration)); + var ex2 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration, testException)); + var ex3 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "FirstRefresh", "Retriever")); + var ex4 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "ConfigurationRetrievalFailed", "Retriever", testException)); + var ex5 = Record.Exception(() => telemetryClient.LogBackgroundConfigurationRefreshFailure(testAddress, "Retriever", testException)); + + Assert.Null(ex1); + Assert.Null(ex2); + Assert.Null(ex3); + Assert.Null(ex4); + Assert.Null(ex5); + } + + [Fact] + public void TelemetryClient_WithAppContextSwitch_DoesNotThrow() + { + // Test that telemetry methods work correctly when the backward compatibility switch is enabled + try + { + // Enable the switch to use full metadata address + AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, true); + + // Arrange + var telemetryClient = new TelemetryClient(); + var testAddress = "https://accounts.google.com/.well-known/openid-configuration"; + var testDuration = TimeSpan.FromMilliseconds(150); + var testException = new InvalidOperationException("Test exception"); + + // Act & Assert - these should not throw even with the switch enabled + var ex1 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration)); + var ex2 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration, testException)); + var ex3 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "FirstRefresh", "Retriever")); + var ex4 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "ConfigurationRetrievalFailed", "Retriever", testException)); + var ex5 = Record.Exception(() => telemetryClient.LogBackgroundConfigurationRefreshFailure(testAddress, "Retriever", testException)); + + Assert.Null(ex1); + Assert.Null(ex2); + Assert.Null(ex3); + Assert.Null(ex4); + Assert.Null(ex5); + } + finally + { + // Cleanup + AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, false); + } + } +}