From ead42019619f6fb2e33d57bd4baccdf19c6fb17f Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Wed, 8 Jan 2025 13:00:06 -0800 Subject: [PATCH 1/9] fix warnings (#3081) --- .../Microsoft.IdentityModel.Benchmarks.csproj | 5 +++++ build/commonTest.props | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/Microsoft.IdentityModel.Benchmarks.csproj b/benchmark/Microsoft.IdentityModel.Benchmarks/Microsoft.IdentityModel.Benchmarks.csproj index c7d383a7e1..2cbc222427 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/Microsoft.IdentityModel.Benchmarks.csproj +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/Microsoft.IdentityModel.Benchmarks.csproj @@ -47,4 +47,9 @@ + + + true + + diff --git a/build/commonTest.props b/build/commonTest.props index 488f6ef6f3..74189e5955 100644 --- a/build/commonTest.props +++ b/build/commonTest.props @@ -49,5 +49,10 @@ + + + + true + From 36ec5c0fefdf6d350aa222f67e19e2fb94883756 Mon Sep 17 00:00:00 2001 From: Sergey Maslov Date: Thu, 9 Jan 2025 01:56:08 +0300 Subject: [PATCH 2/9] Fixed integer overflow in AuthenticatedEncryptionProvider.cs (#3063) An overflow in the arithmetic expression authenticatedData.Length * 8 with type int(32 bits, signed) can occur before casting into wider type long(64 bits, signed) --- .../Encryption/AuthenticatedEncryptionProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs b/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs index a8e38b0b1f..6965c1f0b6 100644 --- a/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs +++ b/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs @@ -152,7 +152,7 @@ private AuthenticatedEncryptionResult EncryptWithAesCbc(byte[] plaintext, byte[] throw LogHelper.LogExceptionMessage(new SecurityTokenEncryptionFailedException(LogHelper.FormatInvariant(LogMessages.IDX10654, ex))); } - byte[] al = Utility.ConvertToBigEndian(authenticatedData.Length * 8); + byte[] al = Utility.ConvertToBigEndian(authenticatedData.Length * 8L); byte[] macBytes = new byte[authenticatedData.Length + aes.IV.Length + ciphertext.Length + al.Length]; Array.Copy(authenticatedData, 0, macBytes, 0, authenticatedData.Length); Array.Copy(aes.IV, 0, macBytes, authenticatedData.Length, aes.IV.Length); @@ -173,7 +173,7 @@ private byte[] DecryptWithAesCbc(byte[] ciphertext, byte[] authenticatedData, by throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException( LogHelper.FormatInvariant(LogMessages.IDX10625, authenticationTag.Length, expectedTagLength, Base64UrlEncoder.Encode(authenticationTag), Algorithm))); - byte[] al = Utility.ConvertToBigEndian(authenticatedData.Length * 8); + byte[] al = Utility.ConvertToBigEndian(authenticatedData.Length * 8L); byte[] macBytes = new byte[authenticatedData.Length + iv.Length + ciphertext.Length + al.Length]; Array.Copy(authenticatedData, 0, macBytes, 0, authenticatedData.Length); Array.Copy(iv, 0, macBytes, authenticatedData.Length, iv.Length); From 5090280f2777474c956d5672e69df767c2e823e8 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:24:32 -0800 Subject: [PATCH 3/9] Test updates. (#3080) --- .../CustomJsonWebToken.cs | 41 +++++++++++++------ .../JsonWebTokenTests.cs | 9 ++-- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs index 813b82e4b3..f0e03afcf1 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs @@ -1,13 +1,27 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Text.Json; -using Microsoft.IdentityModel.Tokens.Json; namespace Microsoft.IdentityModel.JsonWebTokens.Tests { public class CustomJsonWebToken : JsonWebToken { - private const string CustomClaimName = "CustomClaim"; + // Represents claims known to this custom implementation and not to the IdentityModel. + public const string CustomClaimName = "CustomClaim"; + + private CustomClaim _customClaim; + + public CustomClaim CustomClaim + { + get + { + _customClaim ??= Payload.GetValue(CustomClaimName); + return _customClaim; + } + } public CustomJsonWebToken(string jwtEncodedString) : base(jwtEncodedString) { } @@ -17,26 +31,29 @@ public CustomJsonWebToken(string header, string payload) : base(header, payload) private protected override void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) { + // Handle custom claims. if (reader.ValueTextEquals(CustomClaimName)) { - _customClaim = JsonSerializerPrimitives.ReadString(ref reader, CustomClaimName, ClassName, true); + // Deserialize the custom object claim in an appropriate way. + reader.Read(); // Move to the value. + _customClaim = JsonSerializer.Deserialize(reader.GetString()); claims[CustomClaimName] = _customClaim; + reader.Read(); } else { + // Call base implementation to handle other claims known to IdentityModel. base.ReadPayloadValue(ref reader, claims); } } + } - private string _customClaim; - - public string CustomClaim + public class CustomClaim + { + public CustomClaim() { - get - { - _customClaim ??= Payload.GetStringValue(CustomClaimName); - return _customClaim; - } } + + public string CustomClaimValue { get; set; } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index e0c2133600..185cb1f860 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1743,19 +1743,22 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the [Fact] public void DerivedJsonWebToken_IsCreatedCorrectly() { - var expectedCustomClaim = "customclaim"; + var expectedCustomClaim = new CustomClaim() { CustomClaimValue = "customclaim" }; var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Issuer = Default.Issuer, Claims = new Dictionary { - { "CustomClaim", expectedCustomClaim }, + { CustomJsonWebToken.CustomClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomClaim) }, } }); var derivedToken = new CustomJsonWebToken(tokenStr); + derivedToken.TryGetPayloadValue( + CustomJsonWebToken.CustomClaimName, out CustomClaim customClaim); - Assert.Equal(expectedCustomClaim, derivedToken.CustomClaim); + Assert.Equal(expectedCustomClaim.CustomClaimValue, derivedToken.CustomClaim.CustomClaimValue); + Assert.Equal(expectedCustomClaim.CustomClaimValue, customClaim.CustomClaimValue); Assert.Equal(Default.Issuer, derivedToken.Issuer); } From 3f2fbf19a2544030d5b89082990fe874bf18df39 Mon Sep 17 00:00:00 2001 From: Keegan Date: Thu, 9 Jan 2025 11:32:30 -0800 Subject: [PATCH 4/9] Revert change to make RequestRefresh run in the background (#3083) RequestRefresh is a sync api, it is expected that the operation be done when the method returns. With RequestRefresh being on a background thread, callers can experience unexpected behavior. Non blocking RequestRefresh should be done with issue 3040 --- .../Configuration/ConfigurationManager.cs | 2 +- .../ConfigurationManagerTests.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index 1f38022f33..ed3d4b7e67 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -316,7 +316,7 @@ public override void RequestRefresh() _isFirstRefreshRequest = false; if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + UpdateCurrentConfiguration(); _lastRequestRefresh = now; } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index 83d7f5d69c..2fba523106 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -230,7 +230,7 @@ public async Task VerifyInterlockGuardForRequestRefresh() // Interlocked guard will block. // Configuration should be AADCommonV1Config signalEvent.Reset(); - configurationManager.RequestRefresh(); + _ = Task.Run(() => configurationManager.RequestRefresh()); // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished. @@ -239,7 +239,7 @@ public async Task VerifyInterlockGuardForRequestRefresh() // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event. configurationManager.MetadataAddress = "AADCommonV2Json"; TestUtilities.SetField(configurationManager, "_lastRequestRefresh", DateTimeOffset.MinValue); - configurationManager.RequestRefresh(); + _ = Task.Run(() => configurationManager.RequestRefresh()); // Set the event to release the lock and let the previous retriever finish. waitEvent.Set(); @@ -658,14 +658,13 @@ public async Task GetConfigurationAsync() var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); configManager.MetadataAddress = "http://127.0.0.1"; + configManager.RequestRefresh(); var configuration2 = await configManager.GetConfigurationAsync(CancellationToken.None); IdentityComparer.AreEqual(configuration, configuration2, context); if (!object.ReferenceEquals(configuration, configuration2)) context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); - // get configuration from http address, should throw // get configuration with unsuccessful HTTP response status code TestUtilities.AssertFailIfErrors(context); From 6146f1feca4e410c576917bc1b8a37a2ee5196e0 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Thu, 9 Jan 2025 13:41:12 -0800 Subject: [PATCH 5/9] For net4.6.2 select RSACng for PSS support. (#3085) Co-authored-by: id4s --- src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs | 3 +++ .../SignatureProviderTests.cs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs b/src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs index 7b09c89324..8aa11d9318 100644 --- a/src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs +++ b/src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs @@ -241,6 +241,9 @@ private void InitializeUsingRsaSecurityKey(RsaSecurityKey rsaSecurityKey, string { #if NET472 || NET6_0_OR_GREATER var rsa = RSA.Create(rsaSecurityKey.Parameters); +#elif NET462 + var rsa = new RSACng(); + rsa.ImportParameters(rsaSecurityKey.Parameters); #else var rsa = RSA.Create(); rsa.ImportParameters(rsaSecurityKey.Parameters); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs index 1afd38f4a4..28825138c2 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/SignatureProviderTests.cs @@ -1307,7 +1307,7 @@ internal static void AddSignUsingOffsets(byte[] bytes, SecurityKey securityKey, { Bytes = bytes, Count = -1, - ExpectedException = ExpectedException.ArgumentException(), + ExpectedException = prefix == "RSA" ? ExpectedException.ArgumentOutOfRangeException() : ExpectedException.ArgumentException(), Offset = 0, SignatureProvider = CreateProvider(securityKey, algorithm) }); @@ -1316,7 +1316,7 @@ internal static void AddSignUsingOffsets(byte[] bytes, SecurityKey securityKey, { Bytes = bytes, Count = bytes.Length + 1, - ExpectedException = ExpectedException.ArgumentException(), + ExpectedException = prefix == "RSA" ? ExpectedException.ArgumentOutOfRangeException() : ExpectedException.ArgumentException(), Offset = 0, SignatureProvider = CreateProvider(securityKey, algorithm) }); @@ -1325,7 +1325,7 @@ internal static void AddSignUsingOffsets(byte[] bytes, SecurityKey securityKey, { Bytes = bytes, Count = 10, - ExpectedException = ExpectedException.ArgumentException(), + ExpectedException = prefix == "RSA" ? ExpectedException.ArgumentOutOfRangeException() : ExpectedException.ArgumentException(), Offset = bytes.Length - 1, SignatureProvider = CreateProvider(securityKey, algorithm) }); From 42b0c2cb3cbbfcee9ce9a016db2a58d2ff7318ea Mon Sep 17 00:00:00 2001 From: Keegan Date: Fri, 10 Jan 2025 12:30:31 -0800 Subject: [PATCH 6/9] RequestRefresh back to a signal for GetConfigurationAsync (#3087) * RequestRefresh back to a signal Further reverting the change to RequestRefresh from #2780 This is still too much of a behavioral change. RequestRefresh goes back to its historical function, signalling that the next call to GetConfigurationAsync should request data. This is incorporated with the background thread change to instead do the request as a blocking operation if from a RequestRefresh Part of #3082 * Add time based testing to ConfigurationManagerTests through FakeTimeProvider --- build/dependenciesTest.props | 9 + .../Configuration/ConfigurationManager.cs | 32 ++- .../ConfigurationManagerTests.cs | 184 +++++++++++++----- ...Model.Protocols.OpenIdConnect.Tests.csproj | 7 + 4 files changed, 168 insertions(+), 64 deletions(-) diff --git a/build/dependenciesTest.props b/build/dependenciesTest.props index 65daedcf38..7cc73f438b 100644 --- a/build/dependenciesTest.props +++ b/build/dependenciesTest.props @@ -19,4 +19,13 @@ 3.0.0-pre.49 + + + + 9.0.0 + + + 8.10.0 + + diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index ed3d4b7e67..beec3992c2 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -35,6 +35,15 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private const int ConfigurationRetrieverRunning = 1; private int _configurationRetrieverState = ConfigurationRetrieverIdle; + private readonly TimeProvider _timeProvider = TimeProvider.System; + + // If a refresh is requested, then do the refresh as a blocking operation + // not on a background thread. RequestRefresh signals that the app is explicitly + // requesting a refresh, so it should be done immediately so the next + // call to GetConfiguration will return new configuration if the minimum + // refresh interval has passed. + bool _refreshRequested; + /// /// Instantiates a new that manages automatic and controls refreshing on configuration data. /// @@ -147,7 +156,7 @@ public async Task GetConfigurationAsync() /// If the time since the last call is less than then is not called and the current Configuration is returned. public virtual async Task GetConfigurationAsync(CancellationToken cancel) { - if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow) + if (_currentConfiguration != null && _syncAfter > _timeProvider.GetUtcNow()) return _currentConfiguration; Exception fetchMetadataFailure = null; @@ -214,7 +223,13 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) { if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + if (_refreshRequested) + { + UpdateCurrentConfiguration(); + _refreshRequested = false; + } + else + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); } } @@ -285,7 +300,7 @@ private void UpdateCurrentConfiguration() private void UpdateConfiguration(T configuration) { _currentConfiguration = configuration; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + + _syncAfter = DateTimeUtil.Add(_timeProvider.GetUtcNow().UtcDateTime, AutomaticRefreshInterval + TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); } @@ -309,16 +324,13 @@ public override async Task GetBaseConfigurationAsync(Cancella /// public override void RequestRefresh() { - DateTimeOffset now = DateTimeOffset.UtcNow; - + DateTimeOffset now = _timeProvider.GetUtcNow(); if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; - if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) - { - UpdateCurrentConfiguration(); - _lastRequestRefresh = now; - } + _syncAfter = now; + _lastRequestRefresh = now; + _refreshRequested = true; } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index 2fba523106..caa7e0b6a8 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect.Configuration; using Microsoft.IdentityModel.TestUtils; @@ -19,9 +20,6 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests { - /// - /// - /// public class ConfigurationManagerTests { /// @@ -209,48 +207,6 @@ public async Task FetchMetadataFailureTest() TestUtilities.AssertFailIfErrors(context); } - [Fact] - public async Task VerifyInterlockGuardForRequestRefresh() - { - ManualResetEvent waitEvent = new ManualResetEvent(false); - ManualResetEvent signalEvent = new ManualResetEvent(false); - InMemoryDocumentRetriever inMemoryDocumentRetriever = InMemoryDocumentRetrieverWithEvents(waitEvent, signalEvent); - - var configurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - inMemoryDocumentRetriever); - - // populate the configurationManager with AADCommonV1Config - TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); - - // InMemoryDocumentRetrieverWithEvents will block until waitEvent.Set() is called. - // The first RequestRefresh will not have finished before the next RequestRefresh() is called. - // The guard '_lastRequestRefresh' will not block as we set it to DateTimeOffset.MinValue. - // Interlocked guard will block. - // Configuration should be AADCommonV1Config - signalEvent.Reset(); - _ = Task.Run(() => configurationManager.RequestRefresh()); - - // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress - // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished. - signalEvent.WaitOne(); - - // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event. - configurationManager.MetadataAddress = "AADCommonV2Json"; - TestUtilities.SetField(configurationManager, "_lastRequestRefresh", DateTimeOffset.MinValue); - _ = Task.Run(() => configurationManager.RequestRefresh()); - - // Set the event to release the lock and let the previous retriever finish. - waitEvent.Set(); - - // Configuration should be AADCommonV1Config - var configuration = await configurationManager.GetConfigurationAsync(); - Assert.True(configuration.Issuer.Equals(OpenIdConfigData.AADCommonV1Config.Issuer), - $"configuration.Issuer from configurationManager was not as expected," + - $"configuration.Issuer: '{configuration.Issuer}' != expected '{OpenIdConfigData.AADCommonV1Config.Issuer}'."); - } - [Fact] public async Task VerifyInterlockGuardForGetConfigurationAsync() { @@ -320,13 +276,15 @@ public async Task BootstrapRefreshIntervalTest() catch (Exception firstFetchMetadataFailure) { // _syncAfter should not have been changed, because the fetch failed. - var syncAfter = TestUtilities.GetField(configManager, "_syncAfter"); - if ((DateTimeOffset)syncAfter != DateTimeOffset.MinValue) + DateTimeOffset syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); + if (syncAfter != DateTimeOffset.MinValue) context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal '{DateTimeOffset.MinValue}'."); if (firstFetchMetadataFailure.InnerException == null) context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure."); + DateTime requestTime = DateTime.UtcNow; + // Fetch metadata again during refresh interval, the exception should be same from above. try { @@ -339,9 +297,10 @@ public async Task BootstrapRefreshIntervalTest() context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure."); // _syncAfter should not have been changed, because the fetch failed. - syncAfter = TestUtilities.GetField(configManager, "_syncAfter"); - if ((DateTimeOffset)syncAfter != DateTimeOffset.MinValue) - context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal '{DateTimeOffset.MinValue}'."); + syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); + + if (!IdentityComparer.AreDatesEqualWithEpsilon(requestTime, syncAfter.UtcDateTime, 1)) + context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter.UtcDateTime}' should equal be within 1 second of '{requestTime}'."); IdentityComparer.AreEqual(firstFetchMetadataFailure, secondFetchMetadataFailure, context); } @@ -605,10 +564,10 @@ public static TheoryData("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); @@ -634,9 +593,18 @@ public async Task CheckSyncAfter() // make same check for RequestRefresh // force a refresh by setting internal field TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + configManager.RequestRefresh(); - // wait 1000ms here because update of config is run as a new task. - Thread.Sleep(1000); + + bool refreshRequested = (bool)TestUtilities.GetField(configManager, "_refreshRequested"); + if (!refreshRequested) + context.Diffs.Add("Refresh is expected to be requested after RequestRefresh is called"); + + await configManager.GetConfigurationAsync(); + + refreshRequested = (bool)TestUtilities.GetField(configManager, "_refreshRequested"); + if (refreshRequested) + context.Diffs.Add("Refresh is not expected to be requested after GetConfigurationAsync is called"); // check that _syncAfter is greater than DateTimeOffset.UtcNow + AutomaticRefreshInterval syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); @@ -750,6 +718,114 @@ public void TestConfigurationComparer() TestUtilities.AssertFailIfErrors(context); } + [Fact] + public async Task RequestRefresh_RespectsRefreshInterval() + { + // This test checks that the _syncAfter field is set correctly after a refresh. + var context = new CompareContext($"{this}.RequestRefresh_RespectsRefreshInterval"); + + var timeProvider = new FakeTimeProvider(); + + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + // Get the first configuration. + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); + + configManager.RequestRefresh(); + + var configAfterFirstRefresh = await configManager.GetConfigurationAsync(CancellationToken.None); + + // First RequestRefresh triggers a refresh. + if (object.ReferenceEquals(configuration, configAfterFirstRefresh)) + context.Diffs.Add("object.ReferenceEquals(configuration, configAfterFirstRefresh)"); + + configManager.RequestRefresh(); + + var configAfterNoTimePassed = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Second RequestRefresh should not trigger a refresh because the refresh interval has not passed. + if (!object.ReferenceEquals(configAfterFirstRefresh, configAfterNoTimePassed)) + context.Diffs.Add("!object.ReferenceEquals(configAfterFirstRefresh, configAfterNoTimePassed)"); + + // Advance time to trigger a refresh. + timeProvider.Advance(configManager.RefreshInterval); + + configManager.RequestRefresh(); + + var configAfterRefreshInterval = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Third RequestRefresh should trigger a refresh because the refresh interval has passed. + if (object.ReferenceEquals(configAfterNoTimePassed, configAfterRefreshInterval)) + context.Diffs.Add("object.ReferenceEquals(configAfterNoTimePassed, configAfterRefreshInterval)"); + + // Advance time just prior to a refresh. + timeProvider.Advance(configManager.RefreshInterval.Subtract(TimeSpan.FromSeconds(1))); + + var configAfterLessThanRefreshInterval = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Fourth RequestRefresh should not trigger a refresh because the refresh interval has not passed. + if (!object.ReferenceEquals(configAfterRefreshInterval, configAfterLessThanRefreshInterval)) + context.Diffs.Add("object.ReferenceEquals(configAfterRefreshInterval, configAfterLessThanRefreshInterval)"); + + // Advance time 365 days. + timeProvider.Advance(TimeSpan.FromDays(365)); + + var configAfterOneYear = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Fifth RequestRefresh should trigger a refresh because the refresh interval has passed. + if (!object.ReferenceEquals(configAfterLessThanRefreshInterval, configAfterOneYear)) + context.Diffs.Add("object.ReferenceEquals(configAfterLessThanRefreshInterval, configAfterOneYear)"); + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public async Task GetConfigurationAsync_RespectsRefreshInterval() + { + var context = new CompareContext($"{this}.GetConfigurationAsync_RespectsRefreshInterval"); + + var timeProvider = new FakeTimeProvider(); + + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + TimeSpan advanceInterval = BaseConfigurationManager.DefaultAutomaticRefreshInterval.Add(TimeSpan.FromSeconds(configManager.AutomaticRefreshInterval.TotalSeconds / 20)); + + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + // Get the first configuration. + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); + + var configNoAdvanceInTime = await configManager.GetConfigurationAsync(CancellationToken.None); + + // First GetConfigurationAsync should not trigger a refresh because the refresh interval has not passed. + if (!object.ReferenceEquals(configuration, configNoAdvanceInTime)) + context.Diffs.Add("!object.ReferenceEquals(configuration, configNoAdvanceInTime)"); + + // Advance time to trigger a refresh. + timeProvider.Advance(advanceInterval); + + var configAfterTimeIsAdvanced = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Same config, but a task is queued to update the configuration. + if (!object.ReferenceEquals(configNoAdvanceInTime, configAfterTimeIsAdvanced)) + context.Diffs.Add("!object.ReferenceEquals(configuration, configAfterTimeIsAdvanced)"); + + // Need to wait for background task to finish. + Thread.Sleep(250); + + var configAfterBackgroundTask = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Configuration should be updated after the background task finishes. + if (object.ReferenceEquals(configAfterTimeIsAdvanced, configAfterBackgroundTask)) + context.Diffs.Add("object.ReferenceEquals(configuration, configAfterBackgroundTask)"); + + TestUtilities.AssertFailIfErrors(context); + } + [Theory, MemberData(nameof(ValidateOpenIdConnectConfigurationTestCases), DisableDiscoveryEnumeration = true)] public async Task ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTheoryData theoryData) { diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj index 1cbfb9eed3..8d266dc7ce 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj @@ -11,6 +11,12 @@ true + + + true + + PreserveNewest @@ -27,6 +33,7 @@ + From 495738312e7ba4d44dbef043ababb14559ee2e5f Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Sun, 12 Jan 2025 08:19:58 -0800 Subject: [PATCH 7/9] Enabling symmetric and asymmetric keys to be created publicly with JWK (#3089) * Enabling symmetric and asymmetric keys to be created publicly with JWK * Clean up * Revert "Enabling symmetric and asymmetric keys to be created publicly with JWK" This reverts commit 055721dc61d83d07f1376758ff2176670d4bbbe5. * Making TryConvertToSecurityKey public --------- Co-authored-by: trwalke --- .../JsonWebKeyConverter.cs | 10 +++++++++- .../PublicAPI.Unshipped.txt | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.Tokens/JsonWebKeyConverter.cs b/src/Microsoft.IdentityModel.Tokens/JsonWebKeyConverter.cs index 786d6c1b25..ae41d800b0 100644 --- a/src/Microsoft.IdentityModel.Tokens/JsonWebKeyConverter.cs +++ b/src/Microsoft.IdentityModel.Tokens/JsonWebKeyConverter.cs @@ -199,8 +199,16 @@ public static JsonWebKey ConvertFromECDsaSecurityKey(ECDsaSecurityKey key) } #endif - internal static bool TryConvertToSecurityKey(JsonWebKey webKey, out SecurityKey key) + /// + /// This will attempt to convert the to a . + /// + /// + /// + public static bool TryConvertToSecurityKey(JsonWebKey webKey, out SecurityKey key) { + if (webKey == null) + throw LogHelper.LogArgumentNullException(nameof(webKey)); + if (webKey.ConvertedSecurityKey != null) { key = webKey.ConvertedSecurityKey; diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt index d2c20a77d4..0743567f72 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.IncludeKeyIdInHeader.get -> bool Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor.IncludeKeyIdInHeader.set -> void +static Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey webKey, out Microsoft.IdentityModel.Tokens.SecurityKey key) -> bool From 075a6275a03757f0f136214838a10640cf83945f Mon Sep 17 00:00:00 2001 From: Keegan Date: Mon, 13 Jan 2025 14:34:46 -0800 Subject: [PATCH 8/9] Make updates to syncAfter and lastRefreshRequest atomic (#3090) * Make updates to syncAfter and lastRequestRefresh atomic --------- Co-authored-by: Keegan Caruso --- .../Configuration/ConfigurationManager.cs | 46 +++++++++--- .../ConfigurationManagerTests.cs | 72 +++++++++---------- .../ExtensibilityTests.cs | 2 +- 3 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index beec3992c2..fe6d70ff31 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -3,6 +3,7 @@ using System; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Logging; @@ -18,8 +19,16 @@ namespace Microsoft.IdentityModel.Protocols [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] public class ConfigurationManager : BaseConfigurationManager, IConfigurationManager where T : class { - private DateTimeOffset _syncAfter = DateTimeOffset.MinValue; - private DateTimeOffset _lastRequestRefresh = DateTimeOffset.MinValue; + // To prevent tearing, this needs to be only updated through AtomicUpdateSyncAfter. + // Reads should be done through the property SyncAfter. + private DateTime _syncAfter = DateTime.MinValue; + private DateTime SyncAfter => _syncAfter; + + // See comment above, this should only be updated through AtomicUpdateLastRequestRefresh, + // read through LastRequestRefresh. + private DateTime _lastRequestRefresh = DateTime.MinValue; + private DateTime LastRequestRefresh => _lastRequestRefresh; + private bool _isFirstRefreshRequest = true; private readonly SemaphoreSlim _configurationNullLock = new SemaphoreSlim(1); @@ -156,7 +165,7 @@ public async Task GetConfigurationAsync() /// If the time since the last call is less than then is not called and the current Configuration is returned. public virtual async Task GetConfigurationAsync(CancellationToken cancel) { - if (_currentConfiguration != null && _syncAfter > _timeProvider.GetUtcNow()) + if (_currentConfiguration != null && SyncAfter > _timeProvider.GetUtcNow()) return _currentConfiguration; Exception fetchMetadataFailure = null; @@ -242,7 +251,7 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) LogHelper.FormatInvariant( LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), - LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(SyncAfter), LogHelper.MarkAsNonPII(fetchMetadataFailure)), fetchMetadataFailure)); } @@ -300,8 +309,27 @@ private void UpdateCurrentConfiguration() private void UpdateConfiguration(T configuration) { _currentConfiguration = configuration; - _syncAfter = DateTimeUtil.Add(_timeProvider.GetUtcNow().UtcDateTime, AutomaticRefreshInterval + + var newSyncTime = DateTimeUtil.Add(_timeProvider.GetUtcNow().UtcDateTime, AutomaticRefreshInterval + TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); + AtomicUpdateSyncAfter(newSyncTime); + } + + private void AtomicUpdateSyncAfter(DateTime syncAfter) + { + // DateTime's backing data is safe to treat as a long if the Kind is not local. + // _syncAfter will always be updated to a UTC time. + // See the implementation of ToBinary on DateTime. + Interlocked.Exchange( + ref Unsafe.As(ref _syncAfter), + Unsafe.As(ref syncAfter)); + } + + private void AtomicUpdateLastRequestRefresh(DateTime lastRequestRefresh) + { + // See the comment in AtomicUpdateSyncAfter. + Interlocked.Exchange( + ref Unsafe.As(ref _lastRequestRefresh), + Unsafe.As(ref lastRequestRefresh)); } /// @@ -324,12 +352,12 @@ public override async Task GetBaseConfigurationAsync(Cancella /// public override void RequestRefresh() { - DateTimeOffset now = _timeProvider.GetUtcNow(); - if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) + DateTime now = _timeProvider.GetUtcNow().UtcDateTime; + if (now >= DateTimeUtil.Add(LastRequestRefresh, RefreshInterval) || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; - _syncAfter = now; - _lastRequestRefresh = now; + AtomicUpdateSyncAfter(now); + AtomicUpdateLastRequestRefresh(now); _refreshRequested = true; } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index caa7e0b6a8..1ee7645570 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// Ignore Spelling: Metadata Validator Retreiver +// Ignore Spelling: Metadata Validator using System; using System.Collections.Generic; @@ -37,7 +37,7 @@ public async Task GetPublicMetadata(ConfigurationManagerTheoryData( theoryData.MetadataAddress, - theoryData.ConfigurationRetreiver, + theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); @@ -60,7 +60,7 @@ public static TheoryData("AccountsGoogleCom") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AccountsGoogle @@ -68,7 +68,7 @@ public static TheoryData("AADCommonUrl") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrl @@ -76,7 +76,7 @@ public static TheoryData("AADCommonUrlV1") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV1 @@ -84,7 +84,7 @@ public static TheoryData("AADCommonUrlV2") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV2 @@ -99,7 +99,7 @@ public void OpenIdConnectConstructor(ConfigurationManagerTheoryData(theoryData.MetadataAddress, theoryData.ConfigurationRetreiver, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); + var configurationManager = new ConfigurationManager(theoryData.MetadataAddress, theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); theoryData.ExpectedException.ProcessNoException(); } catch (Exception ex) @@ -118,7 +118,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -129,7 +129,7 @@ public static TheoryData { - ConfigurationRetreiver = null, + ConfigurationRetriever = null, ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -139,7 +139,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -149,7 +149,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = null, DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -232,7 +232,7 @@ public async Task VerifyInterlockGuardForGetConfigurationAsync() waitEvent.Reset(); signalEvent.Reset(); - TestUtilities.SetField(configurationManager, "_syncAfter", DateTimeOffset.MinValue); + TestUtilities.SetField(configurationManager, "_syncAfter", DateTime.MinValue); await configurationManager.GetConfigurationAsync(CancellationToken.None); // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress @@ -276,8 +276,8 @@ public async Task BootstrapRefreshIntervalTest() catch (Exception firstFetchMetadataFailure) { // _syncAfter should not have been changed, because the fetch failed. - DateTimeOffset syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); - if (syncAfter != DateTimeOffset.MinValue) + DateTime syncAfter = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); + if (syncAfter != DateTime.MinValue) context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal '{DateTimeOffset.MinValue}'."); if (firstFetchMetadataFailure.InnerException == null) @@ -297,10 +297,10 @@ public async Task BootstrapRefreshIntervalTest() context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure."); // _syncAfter should not have been changed, because the fetch failed. - syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); + syncAfter = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); - if (!IdentityComparer.AreDatesEqualWithEpsilon(requestTime, syncAfter.UtcDateTime, 1)) - context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter.UtcDateTime}' should equal be within 1 second of '{requestTime}'."); + if (!IdentityComparer.AreDatesEqualWithEpsilon(requestTime, syncAfter, 1)) + context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal be within 1 second of '{requestTime}'."); IdentityComparer.AreEqual(firstFetchMetadataFailure, secondFetchMetadataFailure, context); } @@ -355,10 +355,10 @@ public async Task AutomaticRefreshInterval(ConfigurationManagerTheoryData("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTime.UtcNow.Subtract(TimeSpan.FromHours(1))); configManager.MetadataAddress = "http://127.0.0.1"; configManager.RequestRefresh(); var configuration2 = await configManager.GetConfigurationAsync(CancellationToken.None); @@ -832,7 +832,7 @@ public async Task ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTh TestUtilities.WriteHeader($"{this}.ValidateOpenIdConnectConfigurationTests"); var context = new CompareContext(); OpenIdConnectConfiguration configuration; - var configurationManager = new ConfigurationManager(theoryData.MetadataAddress, theoryData.ConfigurationRetreiver, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); + var configurationManager = new ConfigurationManager(theoryData.MetadataAddress, theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); if (theoryData.PresetCurrentConfiguration) TestUtilities.SetField(configurationManager, "_currentConfiguration", new OpenIdConnectConfiguration() { Issuer = Default.Issuer }); @@ -872,7 +872,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator, DocumentRetriever = new FileDocumentRetriever(), First = true, @@ -882,7 +882,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX21818:", typeof(InvalidConfigurationException)), @@ -892,7 +892,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -903,7 +903,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX10810:", typeof(InvalidConfigurationException)), @@ -913,7 +913,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -924,7 +924,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX21817:", typeof(InvalidConfigurationException)), @@ -934,7 +934,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -945,7 +945,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX10814:", typeof(InvalidConfigurationException)), @@ -955,7 +955,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -1001,7 +1001,7 @@ public ConfigurationManagerTheoryData(string testId) : base(testId) { } public TimeSpan AutomaticRefreshInterval { get; set; } - public IConfigurationRetriever ConfigurationRetreiver { get; set; } + public IConfigurationRetriever ConfigurationRetriever { get; set; } public IConfigurationValidator ConfigurationValidator { get; set; } diff --git a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs index c37f5d04ce..71cf3e66d8 100644 --- a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs @@ -81,7 +81,7 @@ public async Task ConfigurationManagerUsingCustomClass() configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); configManager.RequestRefresh(); configuration = await configManager.GetConfigurationAsync(); - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTime.UtcNow.Subtract(TimeSpan.FromHours(1))); configManager.MetadataAddress = "IssuerMetadata2.json"; // Wait for the refresh to complete. From 9e91a6408b9d2e5e7d56c6a903d9eaaedbdac46b Mon Sep 17 00:00:00 2001 From: Westin Musser <127992899+westin-m@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:00:59 -0800 Subject: [PATCH 9/9] Add request count and duration telemetry (#3022) * Add request count and duration telemetry * Add request count and duration telemetry * small fix * Feedback, remodel timing and counters * Use interface for logging, move constants to class * some interface changes, rework tests * fix naming * address interface feedback * Interface and namespace changes * avoid friend assemblies * add metadata * doc comments for tags * PR feedback * Apply suggestions from code review Co-authored-by: Keegan * change class name and add clarifying comments * Change namespace, move package reference --------- Co-authored-by: George Krechar Co-authored-by: jennyf19 Co-authored-by: Keegan --- build/dependencies.props | 1 + build/dependenciesTest.props | 1 + .../InternalAPI.Unshipped.txt | 1 + .../JsonWebTokenHandler.ValidateToken.cs | 7 + .../Configuration/ConfigurationManager.cs | 53 +++++- .../InternalAPI.Unshipped.txt | 2 + .../InternalsVisibleTo.cs | 4 + .../InternalAPI.Unshipped.txt | 66 +++++++ .../Microsoft.IdentityModel.Tokens.csproj | 4 + .../Telemetry/ITelemetryClient.cs | 28 +++ .../Telemetry/TelemetryClient.cs | 67 +++++++ .../Telemetry/TelemetryConstants.cs | 67 +++++++ .../Telemetry/TelemetryDataRecorder.cs | 51 ++++++ .../InternalAPI.Unshipped.txt | 3 +- .../JwtSecurityTokenHandler.cs | 7 + .../JsonWebTokenHandlerTelemetryTests.cs | 69 ++++++++ ...crosoft.IdentityModel.Logging.Tests.csproj | 2 + .../ConfigurationManagerTelemetryTests.cs | 167 ++++++++++++++++++ ...Model.Protocols.OpenIdConnect.Tests.csproj | 1 + .../Telemetry/MockTelemetryClient.cs | 48 +++++ .../JwtSecurityTokenHandlerTelemetryTests.cs | 65 +++++++ ...stem.IdentityModel.Tokens.Jwt.Tests.csproj | 2 + 22 files changed, 708 insertions(+), 8 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Protocols/InternalsVisibleTo.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Telemetry/ITelemetryClient.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryConstants.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryDataRecorder.cs create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTelemetryTests.cs create mode 100644 test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTelemetryTests.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/MockTelemetryClient.cs create mode 100644 test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTelemetryTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index 9438d4d70e..aa3c765a75 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -8,6 +8,7 @@ 1.0.0 2.0.3 13.0.3 + 6.0.2 4.5.5 4.5.0 8.0.5 diff --git a/build/dependenciesTest.props b/build/dependenciesTest.props index 7cc73f438b..6bcbbd9361 100644 --- a/build/dependenciesTest.props +++ b/build/dependenciesTest.props @@ -7,6 +7,7 @@ 17.11.1 2.0.3 13.0.3 + 1.6.0 4.3.4 4.3.0 8.0.5 diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt index cb23d777b2..7537e1ca98 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler._telemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateToken(string payload, Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor tokenDescriptor) -> string static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, string compressionAlgorithm, System.Collections.Generic.IDictionary additionalHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> string static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor tokenDescriptor) -> string diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 918788932f..d01751bd4c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace Microsoft.IdentityModel.JsonWebTokens @@ -17,6 +18,8 @@ namespace Microsoft.IdentityModel.JsonWebTokens /// This partial class contains methods and logic related to the validation of tokens. public partial class JsonWebTokenHandler : TokenHandler { + internal Telemetry.ITelemetryClient _telemetryClient = new TelemetryClient(); + /// /// Returns a value that indicates if this handler can validate a . /// @@ -511,6 +514,10 @@ await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration) // where a new valid configuration was somehow published during validation time. if (currentConfiguration != null) { + _telemetryClient.IncrementConfigurationRefreshRequestCounter( + validationParameters.ConfigurationManager.MetadataAddress, + TelemetryConstants.Protocols.Lkg); + validationParameters.ConfigurationManager.RequestRefresh(); validationParameters.RefreshBeforeValidation = true; var lastConfig = currentConfiguration; diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index fe6d70ff31..b5e6bf0eba 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -9,6 +9,7 @@ using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; namespace Microsoft.IdentityModel.Protocols { @@ -45,6 +46,7 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private int _configurationRetrieverState = ConfigurationRetrieverIdle; private readonly TimeProvider _timeProvider = TimeProvider.System; + internal ITelemetryClient TelemetryClient = new TelemetryClient(); // If a refresh is requested, then do the refresh as a blocking operation // not on a background thread. RequestRefresh signals that the app is explicitly @@ -53,6 +55,7 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM // refresh interval has passed. bool _refreshRequested; + /// /// Instantiates a new that manages automatic and controls refreshing on configuration data. /// @@ -190,7 +193,7 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) try { // Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation. - // The transport should have it's own timeouts, etc. + // The transport should have its own timeouts, etc. T configuration = await _configRetriever.GetConfigurationAsync( MetadataAddress, _docRetriever, @@ -201,18 +204,29 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) ConfigurationValidationResult result = _configValidator.Validate(configuration); // in this case we have never had a valid configuration, so we will throw an exception if the validation fails if (!result.Succeeded) - throw LogHelper.LogExceptionMessage( - new InvalidConfigurationException( - LogHelper.FormatInvariant( - LogMessages.IDX20810, - result.ErrorMessage))); + { + var ex = new InvalidConfigurationException( + LogHelper.FormatInvariant( + LogMessages.IDX20810, + result.ErrorMessage)); + + throw LogHelper.LogExceptionMessage(ex); + } } + TelemetryClient.IncrementConfigurationRefreshRequestCounter( + MetadataAddress, + TelemetryConstants.Protocols.FirstRefresh); + UpdateConfiguration(configuration); } catch (Exception ex) { fetchMetadataFailure = ex; + TelemetryClient.IncrementConfigurationRefreshRequestCounter( + MetadataAddress, + TelemetryConstants.Protocols.FirstRefresh, + ex); LogHelper.LogExceptionMessage( new InvalidOperationException( @@ -234,11 +248,22 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) { if (_refreshRequested) { + // Log as manual because RequestRefresh was called + TelemetryClient.IncrementConfigurationRefreshRequestCounter( + MetadataAddress, + TelemetryConstants.Protocols.Manual); + UpdateCurrentConfiguration(); _refreshRequested = false; } else + { + TelemetryClient.IncrementConfigurationRefreshRequestCounter( + MetadataAddress, + TelemetryConstants.Protocols.Automatic); + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + } } } @@ -264,6 +289,8 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) private void UpdateCurrentConfiguration() { #pragma warning disable CA1031 // Do not catch general exception types + long startTimestamp = _timeProvider.GetTimestamp(); + try { T configuration = _configRetriever.GetConfigurationAsync( @@ -271,6 +298,11 @@ private void UpdateCurrentConfiguration() _docRetriever, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + var elapsedTime = _timeProvider.GetElapsedTime(startTimestamp); + TelemetryClient.LogConfigurationRetrievalDuration( + MetadataAddress, + elapsedTime); + if (_configValidator == null) { UpdateConfiguration(configuration); @@ -291,6 +323,12 @@ private void UpdateCurrentConfiguration() } catch (Exception ex) { + var elapsedTime = _timeProvider.GetElapsedTime(startTimestamp); + TelemetryClient.LogConfigurationRetrievalDuration( + MetadataAddress, + elapsedTime, + ex); + LogHelper.LogExceptionMessage( new InvalidOperationException( LogHelper.FormatInvariant( @@ -336,7 +374,7 @@ ref Unsafe.As(ref _lastRequestRefresh), /// Obtains an updated version of Configuration. /// /// CancellationToken - /// Configuration of type BaseConfiguration . + /// Configuration of type BaseConfiguration. /// If the time since the last call is less than then is not called and the current Configuration is returned. public override async Task GetBaseConfigurationAsync(CancellationToken cancel) { @@ -353,6 +391,7 @@ public override async Task GetBaseConfigurationAsync(Cancella public override void RequestRefresh() { DateTime now = _timeProvider.GetUtcNow().UtcDateTime; + if (now >= DateTimeUtil.Add(LastRequestRefresh, RefreshInterval) || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; diff --git a/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt index e69de29bb2..98597bed68 100644 --- a/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.IdentityModel.Protocols.ConfigurationManager.TelemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient +Microsoft.IdentityModel.Protocols.ConfigurationManager.TimeProvider -> System.TimeProvider diff --git a/src/Microsoft.IdentityModel.Protocols/InternalsVisibleTo.cs b/src/Microsoft.IdentityModel.Protocols/InternalsVisibleTo.cs new file mode 100644 index 0000000000..5e00856ea6 --- /dev/null +++ b/src/Microsoft.IdentityModel.Protocols/InternalsVisibleTo.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 14d2d3493a..58044612f2 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,3 +1,16 @@ +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.ExceptionTypeTag = "ExceptionType" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.IdentityModelVersionTag = "IdentityModelVersion" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.MetadataAddressTag = "MetadataAddress" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.OperationStatusTag = "OperationStatus" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Automatic = "Automatic" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.ConfigurationInvalid = "ConfigurationInvalid" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.ConfigurationRetrievalFailed = "ConfigurationRetrievalFailed" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.FirstRefresh = "FirstRefresh" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Lkg = "LastKnownGood" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Manual = "Manual" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IdentityModelConfigurationManagerCounterDescription = "Counter capturing configuration manager operations." -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IdentityModelConfigurationManagerCounterName = "IdentityModelConfigurationManager" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TotalDurationHistogramName = "IdentityModelConfigurationRequestTotalDurationInMS" -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10002 = "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10268 = "IDX10268: Unable to validate audience, validationParameters.ValidAudiences.Count == 0." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10269 = "IDX10269: IssuerValidationDelegate threw an exception, see inner exception." -> string @@ -8,7 +21,36 @@ const Microsoft.IdentityModel.Tokens.LogMessages.IDX10273 = "IDX10273: Algorithm const Microsoft.IdentityModel.Tokens.LogMessages.IDX10274 = "IDX10274: IssuerSigningKeyValidationDelegate threw an exception, see inner exception." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10275 = "IDX10275: TokenTypeValidationDelegate threw an exception, see inner exception." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10276 = "IDX10276: TokenReplayValidationDelegate threw an exception, see inner exception." -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.ExceptionTypeTag = "ExceptionType" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.IdentityModelVersionTag = "IdentityModelVersion" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.MetadataAddressTag = "MetadataAddress" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.OperationStatusTag = "OperationStatus" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Automatic = "Automatic" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.ConfigurationInvalid = "ConfigurationInvalid" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.ConfigurationRetrievalFailed = "ConfigurationRetrievalFailed" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.FirstRefresh = "FirstRefresh" -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10277 = "IDX10277: RequireAudience property on ValidationParameters is set to false. Exiting without validating the audience." -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Lkg = "LastKnownGood" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols.Manual = "Manual" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IdentityModelConfigurationManagerCounterDescription = "Counter capturing configuration manager operations." -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IdentityModelConfigurationManagerCounterName = "IdentityModelConfigurationManager" -> string +const Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TotalDurationHistogramName = "IdentityModelConfigurationRequestTotalDurationInMS" -> string +Microsoft.IdentityModel.Telemetry.ITelemetryClient +Microsoft.IdentityModel.Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient +Microsoft.IdentityModel.Telemetry.TelemetryClient.ClientVer -> string +Microsoft.IdentityModel.Telemetry.TelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.TelemetryClient() -> void +Microsoft.IdentityModel.Telemetry.TelemetryConstants +Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols +Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder +Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TelemetryDataRecorder() -> void Microsoft.IdentityModel.Tokens.AlgorithmValidationError Microsoft.IdentityModel.Tokens.AlgorithmValidationError.AlgorithmValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidAlgorithm, System.Exception innerException = null) -> void Microsoft.IdentityModel.Tokens.AlgorithmValidationError.InvalidAlgorithm.get -> string @@ -37,9 +79,25 @@ Microsoft.IdentityModel.Tokens.SecurityTokenInvalidOperationException.SecurityTo Microsoft.IdentityModel.Tokens.SignatureValidationError Microsoft.IdentityModel.Tokens.SignatureValidationError.InnerValidationError.get -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Tokens.SignatureValidationError.SignatureValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, Microsoft.IdentityModel.Tokens.ValidationError innerValidationError = null, System.Exception innerException = null) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient +Microsoft.IdentityModel.Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration) -> void +Microsoft.IdentityModel.Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient +Microsoft.IdentityModel.Telemetry.TelemetryClient.ClientVer -> string +Microsoft.IdentityModel.Telemetry.TelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, System.TimeSpan operationDuration, System.Exception exception) -> void +Microsoft.IdentityModel.Telemetry.TelemetryClient.TelemetryClient() -> void +Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder +Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TelemetryDataRecorder() -> void Microsoft.IdentityModel.Tokens.TokenReplayValidationError Microsoft.IdentityModel.Tokens.TokenReplayValidationError.ExpirationTime.get -> System.DateTime? Microsoft.IdentityModel.Tokens.TokenReplayValidationError.TokenReplayValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.DateTime? expirationTime, System.Exception innerException = null) -> void +Microsoft.IdentityModel.Telemetry.TelemetryConstants +Microsoft.IdentityModel.Telemetry.TelemetryConstants.Protocols Microsoft.IdentityModel.Tokens.TokenTypeValidationError Microsoft.IdentityModel.Tokens.TokenTypeValidationError.InvalidTokenType.get -> string Microsoft.IdentityModel.Tokens.TokenTypeValidationError.TokenTypeValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidTokenType, System.Exception innerException = null) -> void @@ -61,15 +119,23 @@ override Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError.GetExcep override Microsoft.IdentityModel.Tokens.SignatureValidationError.GetException() -> System.Exception override Microsoft.IdentityModel.Tokens.TokenReplayValidationError.GetException() -> System.Exception override Microsoft.IdentityModel.Tokens.TokenTypeValidationError.GetException() -> System.Exception +static Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IncrementConfigurationRefreshRequestCounter(in System.Diagnostics.TagList tagList) -> void +static Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.RecordConfigurationRetrievalDurationHistogram(long requestDurationInMs, in System.Diagnostics.TagList tagList) -> void static Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError static Microsoft.IdentityModel.Tokens.SignatureValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.SignatureValidationError +static Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.IncrementConfigurationRefreshRequestCounter(in System.Diagnostics.TagList tagList) -> void +static Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.RecordConfigurationRetrievalDurationHistogram(long requestDurationInMs, in System.Diagnostics.TagList tagList) -> void static Microsoft.IdentityModel.Tokens.TokenReplayValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.TokenReplayValidationError static Microsoft.IdentityModel.Tokens.TokenTypeValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.TokenTypeValidationError static Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Decode(System.ReadOnlySpan strSpan, System.Span output) -> int static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedString(System.Collections.Generic.IList strings) -> string static Microsoft.IdentityModel.Tokens.ValidationError.GetCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> System.Diagnostics.StackFrame +static readonly Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.ConfigurationManagerCounter -> System.Diagnostics.Metrics.Counter +static readonly Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TotalDurationHistogram -> System.Diagnostics.Metrics.Histogram static readonly Microsoft.IdentityModel.Tokens.LoggingEventId.TokenValidationFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.IdentityModel.Tokens.LoggingEventId.TokenValidationSucceeded -> Microsoft.Extensions.Logging.EventId +static readonly Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.ConfigurationManagerCounter -> System.Diagnostics.Metrics.Counter +static readonly Microsoft.IdentityModel.Telemetry.TelemetryDataRecorder.TotalDurationHistogram -> System.Diagnostics.Metrics.Histogram static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.AlgorithmValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.AudienceValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.IssuerSigningKeyValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj index b342d4f1f2..25eb2bbc34 100644 --- a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj +++ b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj @@ -53,6 +53,10 @@ + + + + diff --git a/src/Microsoft.IdentityModel.Tokens/Telemetry/ITelemetryClient.cs b/src/Microsoft.IdentityModel.Tokens/Telemetry/ITelemetryClient.cs new file mode 100644 index 0000000000..656fb6bdf5 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Telemetry/ITelemetryClient.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Telemetry +{ + internal interface ITelemetryClient + { + internal void LogConfigurationRetrievalDuration( + string metadataAddress, + TimeSpan operationDuration); + + internal void LogConfigurationRetrievalDuration( + string metadataAddress, + TimeSpan operationDuration, + Exception exception); + + internal void IncrementConfigurationRefreshRequestCounter( + string metadataAddress, + string operationStatus); + + internal void IncrementConfigurationRefreshRequestCounter( + string metadataAddress, + string operationStatus, + Exception exception); + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs new file mode 100644 index 0000000000..20cbfcf15b --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Telemetry +{ + /// + /// Prepares s using the provided data and sends them to for recording. + /// + internal class TelemetryClient : ITelemetryClient + { + public string ClientVer = IdentityModelTelemetryUtil.ClientVer; + + public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) + { + var tagList = new TagList() + { + { TelemetryConstants.IdentityModelVersionTag, ClientVer }, + { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.OperationStatusTag, operationStatus } + }; + + TelemetryDataRecorder.IncrementConfigurationRefreshRequestCounter(tagList); + } + + public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, Exception exception) + { + var tagList = new TagList() + { + { TelemetryConstants.IdentityModelVersionTag, ClientVer }, + { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.OperationStatusTag, operationStatus }, + { TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() } + }; + + TelemetryDataRecorder.IncrementConfigurationRefreshRequestCounter(tagList); + } + + public void LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration) + { + var tagList = new TagList() + { + { TelemetryConstants.IdentityModelVersionTag, ClientVer }, + { TelemetryConstants.MetadataAddressTag, metadataAddress }, + }; + + long durationInMilliseconds = (long)operationDuration.TotalMilliseconds; + TelemetryDataRecorder.RecordConfigurationRetrievalDurationHistogram(durationInMilliseconds, tagList); + } + + public void LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration, Exception exception) + { + var tagList = new TagList() + { + { TelemetryConstants.IdentityModelVersionTag, ClientVer }, + { TelemetryConstants.MetadataAddressTag, metadataAddress }, + { TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() } + }; + + long durationInMilliseconds = (long)operationDuration.TotalMilliseconds; + TelemetryDataRecorder.RecordConfigurationRetrievalDurationHistogram(durationInMilliseconds, tagList); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryConstants.cs b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryConstants.cs new file mode 100644 index 0000000000..a4d9449e75 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryConstants.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Telemetry +{ + internal static class TelemetryConstants + { + // Static attribute tags + + /// + /// Telemetry tag indicating the version of the IdentityModel library. + /// + public const string IdentityModelVersionTag = "IdentityModelVersion"; + + /// + /// Telemetry tag indicating the endpoint from which a configuration is retrieved. + /// + public const string MetadataAddressTag = "MetadataAddress"; + + /// + /// Telemetry tag describing the operation being performed. + /// + public const string OperationStatusTag = "OperationStatus"; + + /// + /// Telemetry tag indicating the type of exception that occurred. + /// + public const string ExceptionTypeTag = "ExceptionType"; + + public static class Protocols + { + // Configuration manager refresh statuses + + /// + /// Telemetry tag indicating configuration retrieval after the refresh interval has expired. + /// + public const string Automatic = "Automatic"; + + /// + /// Telemetry tag indicating configuration retrieval per a call to RequestRefresh. + /// + public const string Manual = "Manual"; + + /// + /// Telemetry tag indicating configuration retrieval when there is no previously cached configuration. + /// + public const string FirstRefresh = "FirstRefresh"; + + /// + /// Telemetry tag indicating configuration retrieval when the last known good configuration is needed. + /// + public const string Lkg = "LastKnownGood"; + + // Configuration manager exception types + + /// + /// Telemetry tag indicating that configuration could not be sucessfully validated after retrieval. + /// + public const string ConfigurationInvalid = "ConfigurationInvalid"; + + /// + /// Telemetry tag indicating that configuration could not be retrieved successfully. + /// + public const string ConfigurationRetrievalFailed = "ConfigurationRetrievalFailed"; + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryDataRecorder.cs b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryDataRecorder.cs new file mode 100644 index 0000000000..5023606081 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryDataRecorder.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Microsoft.IdentityModel.Telemetry +{ + /// + /// Pushes telemetry data to the configured or . + /// + internal class TelemetryDataRecorder + { + /// + /// Meter name for MicrosoftIdentityModel. + /// + private const string MeterName = "MicrosoftIdentityModel_Meter"; + + /// + /// The meter responsible for creating instruments. + /// + private static readonly Meter IdentityModelMeter = new(MeterName, "1.0.0"); + + internal const string TotalDurationHistogramName = "IdentityModelConfigurationRequestTotalDurationInMS"; + + /// + /// Counter to capture configuration refresh requests to ConfigurationManager. + /// + internal const string IdentityModelConfigurationManagerCounterName = "IdentityModelConfigurationManager"; + internal const string IdentityModelConfigurationManagerCounterDescription = "Counter capturing configuration manager operations."; + internal static readonly Counter ConfigurationManagerCounter = IdentityModelMeter.CreateCounter(IdentityModelConfigurationManagerCounterName, description: IdentityModelConfigurationManagerCounterDescription); + + /// + /// Histogram to capture total duration of configuration retrieval by ConfigurationManager in milliseconds. + /// + internal static readonly Histogram TotalDurationHistogram = IdentityModelMeter.CreateHistogram( + TotalDurationHistogramName, + unit: "ms", + description: "Configuration retrieval latency during configuration manager operations."); + + internal static void RecordConfigurationRetrievalDurationHistogram(long requestDurationInMs, in TagList tagList) + { + TotalDurationHistogram.Record(requestDurationInMs, tagList); + } + + internal static void IncrementConfigurationRefreshRequestCounter(in TagList tagList) + { + ConfigurationManagerCounter.Add(1, tagList); + } + } +} diff --git a/src/System.IdentityModel.Tokens.Jwt/InternalAPI.Unshipped.txt b/src/System.IdentityModel.Tokens.Jwt/InternalAPI.Unshipped.txt index 47d5d10352..102ca896eb 100644 --- a/src/System.IdentityModel.Tokens.Jwt/InternalAPI.Unshipped.txt +++ b/src/System.IdentityModel.Tokens.Jwt/InternalAPI.Unshipped.txt @@ -1,2 +1,3 @@ System.IdentityModel.Tokens.Jwt.JwtHeader.JwtHeader(Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, System.Collections.Generic.IDictionary outboundAlgorithmMap, string tokenType, System.Collections.Generic.IDictionary additionalHeaderClaims, bool includeKeyIdInHeader) -> void -System.IdentityModel.Tokens.Jwt.JwtHeader.JwtHeader(Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials, System.Collections.Generic.IDictionary outboundAlgorithmMap, string tokenType, System.Collections.Generic.IDictionary additionalInnerHeaderClaims, bool includeKeyIdInHeader) -> void \ No newline at end of file +System.IdentityModel.Tokens.Jwt.JwtHeader.JwtHeader(Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials, System.Collections.Generic.IDictionary outboundAlgorithmMap, string tokenType, System.Collections.Generic.IDictionary additionalInnerHeaderClaims, bool includeKeyIdInHeader) -> void +System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.TelemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index f108b9dbf5..9c52fef4aa 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -15,6 +15,7 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; namespace System.IdentityModel.Tokens.Jwt @@ -36,6 +37,8 @@ public class JwtSecurityTokenHandler : SecurityTokenHandler private static string _shortClaimType = _namespace + "/ShortTypeName"; private bool _mapInboundClaims = DefaultMapInboundClaims; + internal Microsoft.IdentityModel.Telemetry.ITelemetryClient TelemetryClient = new TelemetryClient(); + /// /// Default claim type mapping for inbound claims. /// @@ -887,6 +890,10 @@ private ClaimsPrincipal ValidateToken(string token, JwtSecurityToken outerToken, // where a new valid configuration was somehow published during validation time. if (currentConfiguration != null) { + TelemetryClient.IncrementConfigurationRefreshRequestCounter( + validationParameters.ConfigurationManager.MetadataAddress, + TelemetryConstants.Protocols.Lkg); + validationParameters.ConfigurationManager.RequestRefresh(); validationParameters.RefreshBeforeValidation = true; var lastConfig = currentConfiguration; diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTelemetryTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTelemetryTests.cs new file mode 100644 index 0000000000..e63b48e9fb --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTelemetryTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; +using Microsoft.IdentityModel.Telemetry.Tests; +using Microsoft.IdentityModel.Validators; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public class JsonWebTokenHandlerTelemetryTests + { + [Fact] + public async Task ValidateJwsWithConfigAsync_ExpectedTagsExist() + { + var invalidIssuerConfig = new OpenIdConnectConfiguration() + { + TokenEndpoint = Default.Issuer + "oauth/token", + Issuer = Default.Issuer + "2" + }; + invalidIssuerConfig.SigningKeys.Add(KeyingMaterial.DefaultX509Key_2048); + + var validationParameters = new TokenValidationParameters + { + ConfigurationManager = new StaticConfigurationManager(invalidIssuerConfig), + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = false + }; + + var testTelemetryClient = new MockTelemetryClient(); + try + { + var handler = new JsonWebTokenHandler() + { + _telemetryClient = testTelemetryClient + }; + var jwt = handler.ReadJsonWebToken(Default.AsymmetricJws); + AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = validationParameters.ConfigurationManager; + var validationResult = await handler.ValidateTokenAsync(jwt, validationParameters); + var rawTokenValidationResult = await handler.ValidateTokenAsync(Default.AsymmetricJws, validationParameters); + } + catch (Exception) + { + // ignore exceptions + } + + var expectedCounterTagList = new Dictionary + { + // metadata address is null because the configuration manager is made using an invalid config to trigger an exception + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.MetadataAddressTag, null }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.Lkg } + }; + + Assert.Equal(expectedCounterTagList, testTelemetryClient.ExportedItems); + } + } +} diff --git a/test/Microsoft.IdentityModel.Logging.Tests/Microsoft.IdentityModel.Logging.Tests.csproj b/test/Microsoft.IdentityModel.Logging.Tests/Microsoft.IdentityModel.Logging.Tests.csproj index 0d29cd28cd..6ed8578f22 100644 --- a/test/Microsoft.IdentityModel.Logging.Tests/Microsoft.IdentityModel.Logging.Tests.csproj +++ b/test/Microsoft.IdentityModel.Logging.Tests/Microsoft.IdentityModel.Logging.Tests.csproj @@ -23,6 +23,8 @@ + + diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTelemetryTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTelemetryTests.cs new file mode 100644 index 0000000000..583781a2d8 --- /dev/null +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTelemetryTests.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Protocols.Configuration; +using Microsoft.IdentityModel.Protocols.OpenIdConnect.Configuration; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Telemetry; +using Microsoft.IdentityModel.Telemetry.Tests; +using Xunit; + +namespace Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests +{ + public class ConfigurationManagerTelemetryTests + { + [Fact] + public async Task RequestRefresh_ExpectedTagsExist() + { + // arrange + var testTelemetryClient = new MockTelemetryClient(); + var configurationManager = new ConfigurationManager( + OpenIdConfigData.AccountsGoogle, + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(), + new OpenIdConnectConfigurationValidator()) + { + TelemetryClient = testTelemetryClient + }; + var cancel = new CancellationToken(); + + // act + // Retrieve the configuration for the first time + await configurationManager.GetConfigurationAsync(cancel); + testTelemetryClient.ClearExportedItems(); + + // Manually request a config refresh + configurationManager.RequestRefresh(); + await configurationManager.GetConfigurationAsync(cancel); + + // assert + var expectedCounterTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.AccountsGoogle }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.Manual }, + }; + + var expectedHistogramTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.AccountsGoogle }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer } + }; + + Assert.Equal(expectedCounterTagList, testTelemetryClient.ExportedItems); + Assert.Equal(expectedHistogramTagList, testTelemetryClient.ExportedHistogramItems); + } + + [Theory, MemberData(nameof(GetConfiguration_ExpectedTagList_TheoryData), DisableDiscoveryEnumeration = true)] + public async Task GetConfigurationAsync_ExpectedTagsExist(ConfigurationManagerTelemetryTheoryData theoryData) + { + var testTelemetryClient = new MockTelemetryClient(); + + var configurationManager = new ConfigurationManager( + theoryData.MetadataAddress, + new OpenIdConnectConfigurationRetriever(), + theoryData.DocumentRetriever, + theoryData.ConfigurationValidator) + { + TelemetryClient = testTelemetryClient + }; + + try + { + await configurationManager.GetConfigurationAsync(); + if (theoryData.SyncAfter != null) + { + testTelemetryClient.ClearExportedItems(); + TestUtilities.SetField(configurationManager, "_syncAfter", theoryData.SyncAfter); + await configurationManager.GetConfigurationAsync(); + } + + } + catch (Exception) + { + // Ignore exceptions + } + + Assert.Equal(theoryData.ExpectedTagList, testTelemetryClient.ExportedItems); + } + + public static TheoryData> GetConfiguration_ExpectedTagList_TheoryData() + { + return new TheoryData> + { + new ConfigurationManagerTelemetryTheoryData("Success-retrieve from endpoint") + { + MetadataAddress = OpenIdConfigData.AccountsGoogle, + ConfigurationValidator = new OpenIdConnectConfigurationValidator(), + ExpectedTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.AccountsGoogle }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.FirstRefresh }, + } + }, + new ConfigurationManagerTelemetryTheoryData("Failure-invalid metadata address") + { + MetadataAddress = OpenIdConfigData.HttpsBadUri, + ConfigurationValidator = new OpenIdConnectConfigurationValidator(), + ExpectedTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.HttpsBadUri }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.FirstRefresh }, + { TelemetryConstants.ExceptionTypeTag, new IOException().GetType().ToString() }, + } + }, + new ConfigurationManagerTelemetryTheoryData("Failure-invalid config") + { + MetadataAddress = OpenIdConfigData.JsonFile, + DocumentRetriever = new FileDocumentRetriever(), + // The config being loaded has two keys; require three to force invalidity + ConfigurationValidator = new OpenIdConnectConfigurationValidator() { MinimumNumberOfKeys = 3 }, + ExpectedTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.JsonFile }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.FirstRefresh }, + { TelemetryConstants.ExceptionTypeTag, new InvalidConfigurationException().GetType().ToString() }, + } + }, + new ConfigurationManagerTelemetryTheoryData("Success-refresh") + { + MetadataAddress = OpenIdConfigData.AADCommonUrl, + ConfigurationValidator = new OpenIdConnectConfigurationValidator(), + SyncAfter = DateTime.UtcNow - TimeSpan.FromDays(2), + ExpectedTagList = new Dictionary + { + { TelemetryConstants.MetadataAddressTag, OpenIdConfigData.AADCommonUrl }, + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.Automatic }, + } + }, + }; + } + } + + public class ConfigurationManagerTelemetryTheoryData : TheoryDataBase where T : class + { + public ConfigurationManagerTelemetryTheoryData(string testId) : base(testId) { } + + public string MetadataAddress { get; set; } + + public IDocumentRetriever DocumentRetriever { get; set; } = new HttpDocumentRetriever(); + + public IConfigurationValidator ConfigurationValidator { get; set; } + + public DateTime? SyncAfter { get; set; } = null; + + public Dictionary ExpectedTagList { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj index 8d266dc7ce..aa78636b2d 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/MockTelemetryClient.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/MockTelemetryClient.cs new file mode 100644 index 0000000000..ba76c4c0fe --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Telemetry/MockTelemetryClient.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Telemetry.Tests +{ + public class MockTelemetryClient : ITelemetryClient + { + public Dictionary ExportedItems = new Dictionary(); + public Dictionary ExportedHistogramItems = new Dictionary(); + + public void ClearExportedItems() + { + ExportedItems.Clear(); + } + + public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) + { + ExportedItems.Add(TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer); + ExportedItems.Add(TelemetryConstants.MetadataAddressTag, metadataAddress); + ExportedItems.Add(TelemetryConstants.OperationStatusTag, operationStatus); + } + + public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, Exception exception) + { + ExportedItems.Add(TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer); + ExportedItems.Add(TelemetryConstants.MetadataAddressTag, metadataAddress); + ExportedItems.Add(TelemetryConstants.OperationStatusTag, operationStatus); + ExportedItems.Add(TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString()); + } + + public void LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration) + { + ExportedHistogramItems.Add(TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer); + ExportedHistogramItems.Add(TelemetryConstants.MetadataAddressTag, metadataAddress); + } + + public void LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration, Exception exception) + { + ExportedHistogramItems.Add(TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer); + ExportedHistogramItems.Add(TelemetryConstants.MetadataAddressTag, metadataAddress); + ExportedHistogramItems.Add(TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString()); + } + } +} diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTelemetryTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTelemetryTests.cs new file mode 100644 index 0000000000..ddef0bde36 --- /dev/null +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTelemetryTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; +using Microsoft.IdentityModel.Telemetry.Tests; +using Microsoft.IdentityModel.Validators; +using Xunit; + +namespace System.IdentityModel.Tokens.Jwt.Tests +{ + public class JwtSecurityTokenHandlerTelemetryTests + { + [Fact] + public void ValidateToken_ExpectedTagsExist() + { + var invalidIssuerConfig = new OpenIdConnectConfiguration() + { + TokenEndpoint = Default.Issuer + "oauth/token", + Issuer = Default.Issuer + "2" + }; + invalidIssuerConfig.SigningKeys.Add(KeyingMaterial.DefaultX509Key_2048); + + var validationParameters = new TokenValidationParameters + { + ConfigurationManager = new StaticConfigurationManager(invalidIssuerConfig), + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = false + }; + + var testTelemetryClient = new MockTelemetryClient(); + try + { + AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = validationParameters.ConfigurationManager; + var handler = new JwtSecurityTokenHandler() + { + TelemetryClient = testTelemetryClient + }; + handler.ValidateToken(Default.AsymmetricJws, validationParameters, out _); + } + catch (Exception) + { + // ignore exceptions + } + + var expectedCounterTagList = new Dictionary + { + // metadata address is null because the configuration manager is made using an invalid config to trigger an exception + { TelemetryConstants.IdentityModelVersionTag, IdentityModelTelemetryUtil.ClientVer }, + { TelemetryConstants.MetadataAddressTag, null }, + { TelemetryConstants.OperationStatusTag, TelemetryConstants.Protocols.Lkg }, + }; + + Assert.Equal(expectedCounterTagList, testTelemetryClient.ExportedItems); + } + } +} diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj b/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj index e68913b3b0..55c2caee3c 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/System.IdentityModel.Tokens.Jwt.Tests.csproj @@ -13,8 +13,10 @@ + +